capybara 2.7.0 → 3.35.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +1 -0
  3. data/History.md +1147 -11
  4. data/License.txt +1 -1
  5. data/README.md +252 -131
  6. data/lib/capybara/config.rb +92 -0
  7. data/lib/capybara/cucumber.rb +3 -3
  8. data/lib/capybara/driver/base.rb +52 -21
  9. data/lib/capybara/driver/node.rb +48 -14
  10. data/lib/capybara/dsl.rb +16 -9
  11. data/lib/capybara/helpers.rb +72 -81
  12. data/lib/capybara/minitest/spec.rb +267 -0
  13. data/lib/capybara/minitest.rb +385 -0
  14. data/lib/capybara/node/actions.rb +337 -89
  15. data/lib/capybara/node/base.rb +50 -32
  16. data/lib/capybara/node/document.rb +19 -3
  17. data/lib/capybara/node/document_matchers.rb +22 -24
  18. data/lib/capybara/node/element.rb +388 -125
  19. data/lib/capybara/node/finders.rb +231 -121
  20. data/lib/capybara/node/matchers.rb +503 -217
  21. data/lib/capybara/node/simple.rb +64 -27
  22. data/lib/capybara/queries/ancestor_query.rb +27 -0
  23. data/lib/capybara/queries/base_query.rb +87 -11
  24. data/lib/capybara/queries/current_path_query.rb +24 -24
  25. data/lib/capybara/queries/match_query.rb +15 -10
  26. data/lib/capybara/queries/selector_query.rb +675 -81
  27. data/lib/capybara/queries/sibling_query.rb +26 -0
  28. data/lib/capybara/queries/style_query.rb +45 -0
  29. data/lib/capybara/queries/text_query.rb +88 -20
  30. data/lib/capybara/queries/title_query.rb +9 -11
  31. data/lib/capybara/rack_test/browser.rb +63 -39
  32. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  33. data/lib/capybara/rack_test/driver.rb +26 -16
  34. data/lib/capybara/rack_test/errors.rb +6 -0
  35. data/lib/capybara/rack_test/form.rb +73 -58
  36. data/lib/capybara/rack_test/node.rb +187 -67
  37. data/lib/capybara/rails.rb +4 -8
  38. data/lib/capybara/registration_container.rb +44 -0
  39. data/lib/capybara/registrations/drivers.rb +42 -0
  40. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  41. data/lib/capybara/registrations/servers.rb +45 -0
  42. data/lib/capybara/result.rb +142 -14
  43. data/lib/capybara/rspec/features.rb +17 -42
  44. data/lib/capybara/rspec/matcher_proxies.rb +82 -0
  45. data/lib/capybara/rspec/matchers/base.rb +111 -0
  46. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  47. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  48. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  49. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  50. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  51. data/lib/capybara/rspec/matchers/have_selector.rb +77 -0
  52. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  53. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  54. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  55. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  56. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  57. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  58. data/lib/capybara/rspec/matchers.rb +143 -244
  59. data/lib/capybara/rspec.rb +10 -12
  60. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  61. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  62. data/lib/capybara/selector/css.rb +102 -0
  63. data/lib/capybara/selector/definition/button.rb +63 -0
  64. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  65. data/lib/capybara/selector/definition/css.rb +10 -0
  66. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  67. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  68. data/lib/capybara/selector/definition/element.rb +28 -0
  69. data/lib/capybara/selector/definition/field.rb +40 -0
  70. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  71. data/lib/capybara/selector/definition/file_field.rb +13 -0
  72. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  73. data/lib/capybara/selector/definition/frame.rb +17 -0
  74. data/lib/capybara/selector/definition/id.rb +6 -0
  75. data/lib/capybara/selector/definition/label.rb +62 -0
  76. data/lib/capybara/selector/definition/link.rb +54 -0
  77. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  78. data/lib/capybara/selector/definition/option.rb +27 -0
  79. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  80. data/lib/capybara/selector/definition/select.rb +81 -0
  81. data/lib/capybara/selector/definition/table.rb +109 -0
  82. data/lib/capybara/selector/definition/table_row.rb +21 -0
  83. data/lib/capybara/selector/definition/xpath.rb +5 -0
  84. data/lib/capybara/selector/definition.rb +278 -0
  85. data/lib/capybara/selector/filter.rb +3 -46
  86. data/lib/capybara/selector/filter_set.rb +124 -0
  87. data/lib/capybara/selector/filters/base.rb +77 -0
  88. data/lib/capybara/selector/filters/expression_filter.rb +22 -0
  89. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  90. data/lib/capybara/selector/filters/node_filter.rb +31 -0
  91. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  92. data/lib/capybara/selector/selector.rb +155 -0
  93. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  94. data/lib/capybara/selector.rb +232 -369
  95. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  96. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  97. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  98. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  99. data/lib/capybara/selenium/driver.rb +380 -142
  100. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  101. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  102. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -0
  103. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  104. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  105. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  106. data/lib/capybara/selenium/extensions/find.rb +110 -0
  107. data/lib/capybara/selenium/extensions/html5_drag.rb +228 -0
  108. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  109. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  110. data/lib/capybara/selenium/logger_suppressor.rb +40 -0
  111. data/lib/capybara/selenium/node.rb +528 -97
  112. data/lib/capybara/selenium/nodes/chrome_node.rb +137 -0
  113. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  114. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  115. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  116. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  117. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  118. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  119. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  120. data/lib/capybara/selenium/patches/logs.rb +45 -0
  121. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  122. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  123. data/lib/capybara/server/animation_disabler.rb +63 -0
  124. data/lib/capybara/server/checker.rb +44 -0
  125. data/lib/capybara/server/middleware.rb +71 -0
  126. data/lib/capybara/server.rb +74 -71
  127. data/lib/capybara/session/config.rb +126 -0
  128. data/lib/capybara/session/matchers.rb +44 -27
  129. data/lib/capybara/session.rb +500 -297
  130. data/lib/capybara/spec/fixtures/no_extension +1 -0
  131. data/lib/capybara/spec/public/jquery.js +5 -5
  132. data/lib/capybara/spec/public/offset.js +6 -0
  133. data/lib/capybara/spec/public/test.js +168 -14
  134. data/lib/capybara/spec/session/accept_alert_spec.rb +37 -14
  135. data/lib/capybara/spec/session/accept_confirm_spec.rb +7 -6
  136. data/lib/capybara/spec/session/accept_prompt_spec.rb +38 -10
  137. data/lib/capybara/spec/session/all_spec.rb +179 -59
  138. data/lib/capybara/spec/session/ancestor_spec.rb +88 -0
  139. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +140 -0
  140. data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
  141. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  142. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  143. data/lib/capybara/spec/session/assert_text_spec.rb +258 -0
  144. data/lib/capybara/spec/session/assert_title_spec.rb +93 -0
  145. data/lib/capybara/spec/session/attach_file_spec.rb +154 -48
  146. data/lib/capybara/spec/session/body_spec.rb +12 -13
  147. data/lib/capybara/spec/session/check_spec.rb +168 -41
  148. data/lib/capybara/spec/session/choose_spec.rb +75 -23
  149. data/lib/capybara/spec/session/click_button_spec.rb +243 -175
  150. data/lib/capybara/spec/session/click_link_or_button_spec.rb +57 -32
  151. data/lib/capybara/spec/session/click_link_spec.rb +100 -53
  152. data/lib/capybara/spec/session/current_scope_spec.rb +11 -10
  153. data/lib/capybara/spec/session/current_url_spec.rb +61 -35
  154. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +7 -7
  155. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +5 -4
  156. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +13 -6
  157. data/lib/capybara/spec/session/element/match_css_spec.rb +21 -7
  158. data/lib/capybara/spec/session/element/match_xpath_spec.rb +9 -7
  159. data/lib/capybara/spec/session/element/matches_selector_spec.rb +91 -34
  160. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +23 -0
  161. data/lib/capybara/spec/session/evaluate_script_spec.rb +45 -3
  162. data/lib/capybara/spec/session/execute_script_spec.rb +24 -4
  163. data/lib/capybara/spec/session/fill_in_spec.rb +166 -64
  164. data/lib/capybara/spec/session/find_button_spec.rb +37 -18
  165. data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
  166. data/lib/capybara/spec/session/find_field_spec.rb +57 -34
  167. data/lib/capybara/spec/session/find_link_spec.rb +47 -10
  168. data/lib/capybara/spec/session/find_spec.rb +290 -144
  169. data/lib/capybara/spec/session/first_spec.rb +91 -48
  170. data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
  171. data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
  172. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +116 -0
  173. data/lib/capybara/spec/session/frame/within_frame_spec.rb +112 -0
  174. data/lib/capybara/spec/session/go_back_spec.rb +3 -2
  175. data/lib/capybara/spec/session/go_forward_spec.rb +3 -2
  176. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  177. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  178. data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
  179. data/lib/capybara/spec/session/has_button_spec.rb +76 -19
  180. data/lib/capybara/spec/session/has_css_spec.rb +277 -131
  181. data/lib/capybara/spec/session/has_current_path_spec.rb +98 -26
  182. data/lib/capybara/spec/session/has_field_spec.rb +177 -107
  183. data/lib/capybara/spec/session/has_link_spec.rb +13 -12
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +78 -0
  185. data/lib/capybara/spec/session/has_select_spec.rb +191 -95
  186. data/lib/capybara/spec/session/has_selector_spec.rb +128 -64
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +172 -5
  189. data/lib/capybara/spec/session/has_text_spec.rb +126 -60
  190. data/lib/capybara/spec/session/has_title_spec.rb +35 -12
  191. data/lib/capybara/spec/session/has_xpath_spec.rb +74 -53
  192. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +3 -2
  193. data/lib/capybara/spec/session/html_spec.rb +14 -6
  194. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  195. data/lib/capybara/spec/session/node_spec.rb +1028 -131
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
  197. data/lib/capybara/spec/session/refresh_spec.rb +34 -0
  198. data/lib/capybara/spec/session/reset_session_spec.rb +75 -34
  199. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +2 -1
  200. data/lib/capybara/spec/session/save_and_open_page_spec.rb +3 -2
  201. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +11 -15
  202. data/lib/capybara/spec/session/save_page_spec.rb +42 -55
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +16 -14
  204. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  205. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  206. data/lib/capybara/spec/session/select_spec.rb +112 -85
  207. data/lib/capybara/spec/session/selectors_spec.rb +71 -8
  208. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  209. data/lib/capybara/spec/session/text_spec.rb +38 -23
  210. data/lib/capybara/spec/session/title_spec.rb +17 -5
  211. data/lib/capybara/spec/session/uncheck_spec.rb +71 -12
  212. data/lib/capybara/spec/session/unselect_spec.rb +44 -43
  213. data/lib/capybara/spec/session/visit_spec.rb +99 -32
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +33 -29
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +5 -3
  216. data/lib/capybara/spec/session/window/open_new_window_spec.rb +5 -3
  217. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +39 -30
  218. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +17 -10
  219. data/lib/capybara/spec/session/window/window_spec.rb +121 -73
  220. data/lib/capybara/spec/session/window/windows_spec.rb +12 -10
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +52 -82
  222. data/lib/capybara/spec/session/within_spec.rb +76 -43
  223. data/lib/capybara/spec/spec_helper.rb +67 -33
  224. data/lib/capybara/spec/test_app.rb +85 -36
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/buttons.erb +1 -1
  227. data/lib/capybara/spec/views/fieldsets.erb +1 -1
  228. data/lib/capybara/spec/views/form.erb +227 -20
  229. data/lib/capybara/spec/views/frame_child.erb +10 -2
  230. data/lib/capybara/spec/views/frame_one.erb +2 -1
  231. data/lib/capybara/spec/views/frame_parent.erb +2 -2
  232. data/lib/capybara/spec/views/frame_two.erb +1 -1
  233. data/lib/capybara/spec/views/header_links.erb +1 -1
  234. data/lib/capybara/spec/views/host_links.erb +1 -1
  235. data/lib/capybara/spec/views/initial_alert.erb +10 -0
  236. data/lib/capybara/spec/views/obscured.erb +47 -0
  237. data/lib/capybara/spec/views/offset.erb +32 -0
  238. data/lib/capybara/spec/views/path.erb +1 -1
  239. data/lib/capybara/spec/views/popup_one.erb +1 -1
  240. data/lib/capybara/spec/views/popup_two.erb +1 -1
  241. data/lib/capybara/spec/views/postback.erb +1 -1
  242. data/lib/capybara/spec/views/react.erb +45 -0
  243. data/lib/capybara/spec/views/scroll.erb +20 -0
  244. data/lib/capybara/spec/views/spatial.erb +31 -0
  245. data/lib/capybara/spec/views/tables.erb +69 -2
  246. data/lib/capybara/spec/views/with_animation.erb +82 -0
  247. data/lib/capybara/spec/views/with_base_tag.erb +1 -1
  248. data/lib/capybara/spec/views/with_count.erb +1 -1
  249. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  250. data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
  251. data/lib/capybara/spec/views/with_hover.erb +7 -1
  252. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  253. data/lib/capybara/spec/views/with_html.erb +100 -10
  254. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  255. data/lib/capybara/spec/views/with_html_entities.erb +1 -1
  256. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  257. data/lib/capybara/spec/views/with_js.erb +49 -3
  258. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  259. data/lib/capybara/spec/views/with_namespace.erb +20 -0
  260. data/lib/capybara/spec/views/with_scope.erb +1 -1
  261. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  262. data/lib/capybara/spec/views/with_simple_html.erb +1 -1
  263. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  264. data/lib/capybara/spec/views/with_title.erb +1 -1
  265. data/lib/capybara/spec/views/with_unload_alert.erb +3 -1
  266. data/lib/capybara/spec/views/with_windows.erb +7 -1
  267. data/lib/capybara/spec/views/within_frames.erb +6 -3
  268. data/lib/capybara/version.rb +2 -1
  269. data/lib/capybara/window.rb +39 -21
  270. data/lib/capybara.rb +208 -186
  271. data/spec/basic_node_spec.rb +52 -39
  272. data/spec/capybara_spec.rb +72 -50
  273. data/spec/css_builder_spec.rb +101 -0
  274. data/spec/css_splitter_spec.rb +38 -0
  275. data/spec/dsl_spec.rb +81 -61
  276. data/spec/filter_set_spec.rb +46 -0
  277. data/spec/fixtures/capybara.csv +1 -0
  278. data/spec/fixtures/certificate.pem +25 -0
  279. data/spec/fixtures/key.pem +27 -0
  280. data/spec/fixtures/selenium_driver_rspec_failure.rb +7 -3
  281. data/spec/fixtures/selenium_driver_rspec_success.rb +7 -3
  282. data/spec/minitest_spec.rb +164 -0
  283. data/spec/minitest_spec_spec.rb +162 -0
  284. data/spec/per_session_config_spec.rb +68 -0
  285. data/spec/rack_test_spec.rb +189 -96
  286. data/spec/regexp_dissassembler_spec.rb +250 -0
  287. data/spec/result_spec.rb +143 -13
  288. data/spec/rspec/features_spec.rb +38 -32
  289. data/spec/rspec/scenarios_spec.rb +9 -7
  290. data/spec/rspec/shared_spec_matchers.rb +959 -0
  291. data/spec/rspec/views_spec.rb +9 -3
  292. data/spec/rspec_matchers_spec.rb +62 -0
  293. data/spec/rspec_spec.rb +127 -30
  294. data/spec/sauce_spec_chrome.rb +43 -0
  295. data/spec/selector_spec.rb +458 -37
  296. data/spec/selenium_spec_chrome.rb +196 -9
  297. data/spec/selenium_spec_chrome_remote.rb +100 -0
  298. data/spec/selenium_spec_edge.rb +47 -0
  299. data/spec/selenium_spec_firefox.rb +210 -0
  300. data/spec/selenium_spec_firefox_remote.rb +80 -0
  301. data/spec/selenium_spec_ie.rb +150 -0
  302. data/spec/selenium_spec_safari.rb +148 -0
  303. data/spec/server_spec.rb +200 -101
  304. data/spec/session_spec.rb +91 -0
  305. data/spec/shared_selenium_node.rb +83 -0
  306. data/spec/shared_selenium_session.rb +558 -0
  307. data/spec/spec_helper.rb +94 -2
  308. data/spec/xpath_builder_spec.rb +93 -0
  309. metadata +420 -60
  310. data/lib/capybara/query.rb +0 -7
  311. data/lib/capybara/spec/session/assert_current_path.rb +0 -60
  312. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  313. data/lib/capybara/spec/session/assert_text.rb +0 -196
  314. data/lib/capybara/spec/session/assert_title.rb +0 -70
  315. data/lib/capybara/spec/session/source_spec.rb +0 -0
  316. data/lib/capybara/spec/session/within_frame_spec.rb +0 -53
  317. data/spec/rspec/matchers_spec.rb +0 -827
  318. data/spec/selenium_spec.rb +0 -151
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module Capybara
6
+ class SessionConfig
7
+ OPTIONS = %i[always_include_port run_server default_selector default_max_wait_time ignore_hidden_elements
8
+ automatic_reload match exact exact_text raise_server_errors visible_text_only
9
+ automatic_label_click enable_aria_label save_path asset_host default_host app_host
10
+ server_host server_port server_errors default_set_options disable_animation test_id
11
+ predicates_wait default_normalize_ws w3c_click_offset enable_aria_role].freeze
12
+
13
+ attr_accessor(*OPTIONS)
14
+
15
+ ##
16
+ # @!method always_include_port
17
+ # See {Capybara.configure}
18
+ # @!method run_server
19
+ # See {Capybara.configure}
20
+ # @!method default_selector
21
+ # See {Capybara.configure}
22
+ # @!method default_max_wait_time
23
+ # See {Capybara.configure}
24
+ # @!method ignore_hidden_elements
25
+ # See {Capybara.configure}
26
+ # @!method automatic_reload
27
+ # See {Capybara.configure}
28
+ # @!method match
29
+ # See {Capybara.configure}
30
+ # @!method exact
31
+ # See {Capybara.configure}
32
+ # @!method raise_server_errors
33
+ # See {Capybara.configure}
34
+ # @!method visible_text_only
35
+ # See {Capybara.configure}
36
+ # @!method automatic_label_click
37
+ # See {Capybara.configure}
38
+ # @!method enable_aria_label
39
+ # See {Capybara.configure}
40
+ # @!method enable_aria_role
41
+ # See {Capybara.configure}
42
+ # @!method save_path
43
+ # See {Capybara.configure}
44
+ # @!method asset_host
45
+ # See {Capybara.configure}
46
+ # @!method default_host
47
+ # See {Capybara.configure}
48
+ # @!method app_host
49
+ # See {Capybara.configure}
50
+ # @!method server_host
51
+ # See {Capybara.configure}
52
+ # @!method server_port
53
+ # See {Capybara.configure}
54
+ # @!method server_errors
55
+ # See {Capybara.configure}
56
+ # @!method default_set_options
57
+ # See {Capybara.configure}
58
+ # @!method disable_animation
59
+ # See {Capybara.configure}
60
+ # @!method test_id
61
+ # See {Capybara.configure}
62
+ # @!method default_normalize_ws
63
+ # See {Capybara.configure}
64
+ # @!method w3c_click_offset
65
+ # See {Capybara.configure}
66
+
67
+ remove_method :server_host
68
+
69
+ ##
70
+ #
71
+ # @return [String] The IP address bound by default server
72
+ #
73
+ def server_host
74
+ @server_host || '127.0.0.1'
75
+ end
76
+
77
+ remove_method :server_errors=
78
+ def server_errors=(errors)
79
+ (@server_errors ||= []).replace(errors.dup)
80
+ end
81
+
82
+ remove_method :app_host=
83
+ def app_host=(url)
84
+ unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
85
+ raise ArgumentError, "Capybara.app_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}."
86
+ end
87
+
88
+ @app_host = url
89
+ end
90
+
91
+ remove_method :default_host=
92
+ def default_host=(url)
93
+ unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
94
+ raise ArgumentError, "Capybara.default_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}."
95
+ end
96
+
97
+ @default_host = url
98
+ end
99
+
100
+ remove_method :test_id=
101
+ ##
102
+ #
103
+ # Set an attribue to be optionally matched against the locator for builtin selector types.
104
+ # This attribute will be checked by builtin selector types whenever id would normally be checked.
105
+ # If `nil` then it will be ignored.
106
+ #
107
+ # @param [String, Symbol, nil] id Name of the attribute to use as the test id
108
+ #
109
+ def test_id=(id)
110
+ @test_id = id&.to_sym
111
+ end
112
+
113
+ def initialize_copy(other)
114
+ super
115
+ @server_errors = @server_errors.dup
116
+ end
117
+ end
118
+
119
+ class ReadOnlySessionConfig < SimpleDelegator
120
+ SessionConfig::OPTIONS.each do |option|
121
+ define_method "#{option}=" do |_|
122
+ raise 'Per session settings are only supported when Capybara.threadsafe == true'
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,70 +1,87 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Capybara
3
4
  module SessionMatchers
4
5
  ##
5
6
  # Asserts that the page has the given path.
6
- # By default this will compare against the path+query portion of the full url
7
+ # By default, if passed a full url this will compare against the full url,
8
+ # if passed a path only the path+query portion will be compared, if passed a regexp
9
+ # the comparison will depend on the :url option (path+query by default)
7
10
  #
8
11
  # @!macro current_path_query_params
9
- # @overload $0(string, options = {})
12
+ # @overload $0(string, **options)
10
13
  # @param string [String] The string that the current 'path' should equal
11
- # @overload $0(regexp, options = {})
14
+ # @overload $0(regexp, **options)
12
15
  # @param regexp [Regexp] The regexp that the current 'path' should match to
13
- # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current path to eq/match given string/regexp argument
14
- # @option options [Boolean] :url (false) Whether the compare should be done against the full url
15
- # @option options [Boolean] :only_path (false) Whether the compare should be done against just the path protion of the url
16
+ # @option options [Boolean] :url (true if `string` is a full url, otherwise false) Whether the comparison should be done against the full current url or just the path
17
+ # @option options [Boolean] :ignore_query (false) Whether the query portion of the current url/path should be ignored
18
+ # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current url/path to eq/match given string/regexp argument
16
19
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
17
20
  # @return [true]
18
21
  #
19
- def assert_current_path(path, options={})
20
- query = Capybara::Queries::CurrentPathQuery.new(path, options)
21
- document.synchronize(query.wait) do
22
- unless query.resolves_for?(self)
23
- raise Capybara::ExpectationNotMet, query.failure_message
24
- end
22
+ def assert_current_path(path, **options, &optional_filter_block)
23
+ _verify_current_path(path, optional_filter_block, **options) do |query|
24
+ raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self)
25
25
  end
26
- return true
27
26
  end
28
27
 
29
28
  ##
30
29
  # Asserts that the page doesn't have the given path.
30
+ # By default, if passed a full url this will compare against the full url,
31
+ # if passed a path only the path+query portion will be compared, if passed a regexp
32
+ # the comparison will depend on the :url option
31
33
  #
32
34
  # @macro current_path_query_params
33
35
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
34
36
  # @return [true]
35
37
  #
36
- def assert_no_current_path(path, options={})
37
- query = Capybara::Queries::CurrentPathQuery.new(path, options)
38
- document.synchronize(query.wait) do
39
- if query.resolves_for?(self)
40
- raise Capybara::ExpectationNotMet, query.negative_failure_message
41
- end
38
+ def assert_no_current_path(path, **options, &optional_filter_block)
39
+ _verify_current_path(path, optional_filter_block, **options) do |query|
40
+ raise Capybara::ExpectationNotMet, query.negative_failure_message if query.resolves_for?(self)
42
41
  end
43
- return true
44
42
  end
45
43
 
46
44
  ##
47
45
  # Checks if the page has the given path.
46
+ # By default, if passed a full url this will compare against the full url,
47
+ # if passed a path only the path+query portion will be compared, if passed a regexp
48
+ # the comparison will depend on the :url option
48
49
  #
49
50
  # @macro current_path_query_params
50
51
  # @return [Boolean]
51
52
  #
52
- def has_current_path?(path, options={})
53
- assert_current_path(path, options)
54
- rescue Capybara::ExpectationNotMet
55
- return false
53
+ def has_current_path?(path, **options, &optional_filter_block)
54
+ make_predicate(options) { assert_current_path(path, **options, &optional_filter_block) }
56
55
  end
57
56
 
58
57
  ##
59
58
  # Checks if the page doesn't have the given path.
59
+ # By default, if passed a full url this will compare against the full url,
60
+ # if passed a path only the path+query portion will be compared, if passed a regexp
61
+ # the comparison will depend on the :url option
60
62
  #
61
63
  # @macro current_path_query_params
62
64
  # @return [Boolean]
63
65
  #
64
- def has_no_current_path?(path, options={})
65
- assert_no_current_path(path, options)
66
+ def has_no_current_path?(path, **options, &optional_filter_block)
67
+ make_predicate(options) { assert_no_current_path(path, **options, &optional_filter_block) }
68
+ end
69
+
70
+ private
71
+
72
+ def _verify_current_path(path, filter_block, **options)
73
+ query = Capybara::Queries::CurrentPathQuery.new(path, **options, &filter_block)
74
+ document.synchronize(query.wait) do
75
+ yield(query)
76
+ end
77
+ true
78
+ end
79
+
80
+ def make_predicate(options)
81
+ options[:wait] = 0 unless options.key?(:wait) || config.predicates_wait
82
+ yield
66
83
  rescue Capybara::ExpectationNotMet
67
- return false
84
+ false
68
85
  end
69
86
  end
70
87
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'capybara/session/matchers'
4
+ require 'addressable/uri'
3
5
 
4
6
  module Capybara
5
-
6
7
  ##
7
8
  #
8
- # The Session class represents a single user's interaction with the system. The Session can use
9
+ # The {Session} class represents a single user's interaction with the system. The {Session} can use
9
10
  # any of the underlying drivers. A session can be initialized manually like this:
10
11
  #
11
12
  # session = Capybara::Session.new(:culerity, MyRackApp)
@@ -16,100 +17,120 @@ module Capybara
16
17
  # session = Capybara::Session.new(:culerity)
17
18
  # session.visit('http://www.google.com')
18
19
  #
19
- # Session provides a number of methods for controlling the navigation of the page, such as +visit+,
20
- # +current_path, and so on. It also delegate a number of methods to a Capybara::Document, representing
20
+ # When {Capybara.configure threadsafe} is `true` the sessions options will be initially set to the
21
+ # current values of the global options and a configuration block can be passed to the session initializer.
22
+ # For available options see {Capybara::SessionConfig::OPTIONS}:
23
+ #
24
+ # session = Capybara::Session.new(:driver, MyRackApp) do |config|
25
+ # config.app_host = "http://my_host.dev"
26
+ # end
27
+ #
28
+ # The {Session} provides a number of methods for controlling the navigation of the page, such as {#visit},
29
+ # {#current_path}, and so on. It also delegates a number of methods to a {Capybara::Document}, representing
21
30
  # the current HTML document. This allows interaction:
22
31
  #
23
- # session.fill_in('q', :with => 'Capybara')
32
+ # session.fill_in('q', with: 'Capybara')
24
33
  # session.click_button('Search')
25
34
  # expect(session).to have_content('Capybara')
26
35
  #
27
- # When using capybara/dsl, the Session is initialized automatically for you.
36
+ # When using `capybara/dsl`, the {Session} is initialized automatically for you.
28
37
  #
29
38
  class Session
30
39
  include Capybara::SessionMatchers
31
40
 
32
- NODE_METHODS = [
33
- :all, :first, :attach_file, :text, :check, :choose,
34
- :click_link_or_button, :click_button, :click_link, :field_labeled,
35
- :fill_in, :find, :find_all, :find_button, :find_by_id, :find_field, :find_link,
36
- :has_content?, :has_text?, :has_css?, :has_no_content?, :has_no_text?,
37
- :has_no_css?, :has_no_xpath?, :resolve, :has_xpath?, :select, :uncheck,
38
- :has_link?, :has_no_link?, :has_button?, :has_no_button?, :has_field?,
39
- :has_no_field?, :has_checked_field?, :has_unchecked_field?,
40
- :has_no_table?, :has_table?, :unselect, :has_select?, :has_no_select?,
41
- :has_selector?, :has_no_selector?, :click_on, :has_no_checked_field?,
42
- :has_no_unchecked_field?, :query, :assert_selector, :assert_no_selector,
43
- :refute_selector, :assert_text, :assert_no_text
44
- ]
41
+ NODE_METHODS = %i[
42
+ all first attach_file text check choose scroll_to scroll_by
43
+ click_link_or_button click_button click_link
44
+ fill_in find find_all find_button find_by_id find_field find_link
45
+ has_content? has_text? has_css? has_no_content? has_no_text?
46
+ has_no_css? has_no_xpath? has_xpath? select uncheck
47
+ has_link? has_no_link? has_button? has_no_button? has_field?
48
+ has_no_field? has_checked_field? has_unchecked_field?
49
+ has_no_table? has_table? unselect has_select? has_no_select?
50
+ has_selector? has_no_selector? click_on has_no_checked_field?
51
+ has_no_unchecked_field? query assert_selector assert_no_selector
52
+ assert_all_of_selectors assert_none_of_selectors assert_any_of_selectors
53
+ refute_selector assert_text assert_no_text
54
+ ].freeze
45
55
  # @api private
46
- DOCUMENT_METHODS = [
47
- :title, :assert_title, :assert_no_title, :has_title?, :has_no_title?
48
- ]
49
- SESSION_METHODS = [
50
- :body, :html, :source, :current_url, :current_host, :current_path,
51
- :execute_script, :evaluate_script, :visit, :go_back, :go_forward,
52
- :within, :within_fieldset, :within_table, :within_frame, :current_window,
53
- :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by,
54
- :save_page, :save_and_open_page, :save_screenshot,
55
- :save_and_open_screenshot, :reset_session!, :response_headers,
56
- :status_code, :current_scope,
57
- :assert_current_path, :assert_no_current_path, :has_current_path?, :has_no_current_path?
58
- ] + DOCUMENT_METHODS
59
- MODAL_METHODS = [
60
- :accept_alert, :accept_confirm, :dismiss_confirm, :accept_prompt,
61
- :dismiss_prompt
62
- ]
56
+ DOCUMENT_METHODS = %i[
57
+ title assert_title assert_no_title has_title? has_no_title?
58
+ ].freeze
59
+ SESSION_METHODS = %i[
60
+ body html source current_url current_host current_path
61
+ execute_script evaluate_script visit refresh go_back go_forward send_keys
62
+ within within_element within_fieldset within_table within_frame switch_to_frame
63
+ current_window windows open_new_window switch_to_window within_window window_opened_by
64
+ save_page save_and_open_page save_screenshot
65
+ save_and_open_screenshot reset_session! response_headers
66
+ status_code current_scope
67
+ assert_current_path assert_no_current_path has_current_path? has_no_current_path?
68
+ ].freeze + DOCUMENT_METHODS
69
+ MODAL_METHODS = %i[
70
+ accept_alert accept_confirm dismiss_confirm accept_prompt dismiss_prompt
71
+ ].freeze
63
72
  DSL_METHODS = NODE_METHODS + SESSION_METHODS + MODAL_METHODS
64
73
 
65
74
  attr_reader :mode, :app, :server
66
75
  attr_accessor :synchronized
67
76
 
68
- def initialize(mode, app=nil)
77
+ def initialize(mode, app = nil)
78
+ if app && !app.respond_to?(:call)
79
+ raise TypeError, 'The second parameter to Session::new should be a rack app if passed.'
80
+ end
81
+
82
+ @@instance_created = true # rubocop:disable Style/ClassVars
69
83
  @mode = mode
70
84
  @app = app
71
- if Capybara.run_server and @app and driver.needs_server?
72
- @server = Capybara::Server.new(@app).boot
73
- else
74
- @server = nil
85
+ if block_given?
86
+ raise 'A configuration block is only accepted when Capybara.threadsafe == true' unless Capybara.threadsafe
87
+
88
+ yield config
89
+ end
90
+ @server = if config.run_server && @app && driver.needs_server?
91
+ server_options = { port: config.server_port, host: config.server_host, reportable_errors: config.server_errors }
92
+ server_options[:extra_middleware] = [Capybara::Server::AnimationDisabler] if config.disable_animation
93
+ Capybara::Server.new(@app, **server_options).boot
75
94
  end
76
95
  @touched = false
77
96
  end
78
97
 
79
98
  def driver
80
99
  @driver ||= begin
81
- unless Capybara.drivers.has_key?(mode)
82
- other_drivers = Capybara.drivers.keys.map { |key| key.inspect }
100
+ unless Capybara.drivers[mode]
101
+ other_drivers = Capybara.drivers.names.map(&:inspect)
83
102
  raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
84
103
  end
85
- Capybara.drivers[mode].call(app)
104
+ driver = Capybara.drivers[mode].call(app)
105
+ driver.session = self if driver.respond_to?(:session=)
106
+ driver
86
107
  end
87
108
  end
88
109
 
89
110
  ##
90
111
  #
91
- # Reset the session (i.e. remove cookies and navigate to blank page)
112
+ # Reset the session (i.e. remove cookies and navigate to blank page).
92
113
  #
93
114
  # This method does not:
94
115
  #
95
- # * accept modal dialogs if they are present (Selenium driver now does, others may not)
96
- # * clear browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc.
97
- # * modify state of the driver/underlying browser in any other way
116
+ # * accept modal dialogs if they are present (Selenium driver now does, others may not)
117
+ # * clear browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc.
118
+ # * modify state of the driver/underlying browser in any other way
98
119
  #
99
120
  # as doing so will result in performance downsides and it's not needed to do everything from the list above for most apps.
100
121
  #
101
122
  # If you want to do anything from the list above on a general basis you can:
102
123
  #
103
- # * write RSpec/Cucumber/etc. after hook
104
- # * monkeypatch this method
105
- # * use Ruby's `prepend` method
124
+ # * write RSpec/Cucumber/etc. after hook
125
+ # * monkeypatch this method
126
+ # * use Ruby's `prepend` method
106
127
  #
107
128
  def reset!
108
129
  if @touched
109
130
  driver.reset!
110
131
  @touched = false
111
132
  end
112
- @server.wait_for_pending_requests if @server
133
+ @server&.wait_for_pending_requests
113
134
  raise_server_error!
114
135
  end
115
136
  alias_method :cleanup!, :reset!
@@ -117,19 +138,40 @@ module Capybara
117
138
 
118
139
  ##
119
140
  #
120
- # Raise errors encountered in the server
141
+ # Disconnect from the current driver. A new driver will be instantiated on the next interaction.
142
+ #
143
+ def quit
144
+ @driver.quit if @driver.respond_to? :quit
145
+ @document = @driver = nil
146
+ @touched = false
147
+ @server&.reset_error!
148
+ end
149
+
150
+ ##
151
+ #
152
+ # Raise errors encountered in the server.
121
153
  #
122
154
  def raise_server_error!
123
- raise @server.error if Capybara.raise_server_errors and @server and @server.error
124
- ensure
125
- @server.reset_error! if @server
155
+ return unless @server&.error
156
+
157
+ # Force an explanation for the error being raised as the exception cause
158
+ begin
159
+ if config.raise_server_errors
160
+ raise CapybaraError, 'Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true'
161
+ end
162
+ rescue CapybaraError
163
+ # needed to get the cause set correctly in JRuby -- otherwise we could just do raise @server.error
164
+ raise @server.error, @server.error.message, @server.error.backtrace
165
+ ensure
166
+ @server.reset_error!
167
+ end
126
168
  end
127
169
 
128
170
  ##
129
171
  #
130
- # Returns a hash of response headers. Not supported by all drivers (e.g. Selenium)
172
+ # Returns a hash of response headers. Not supported by all drivers (e.g. Selenium).
131
173
  #
132
- # @return [Hash{String => String}] A hash of response headers.
174
+ # @return [Hash<String, String>] A hash of response headers.
133
175
  #
134
176
  def response_headers
135
177
  driver.response_headers
@@ -137,7 +179,7 @@ module Capybara
137
179
 
138
180
  ##
139
181
  #
140
- # Returns the current HTTP status code as an Integer. Not supported by all drivers (e.g. Selenium)
182
+ # Returns the current HTTP status code as an integer. Not supported by all drivers (e.g. Selenium).
141
183
  #
142
184
  # @return [Integer] Current HTTP status code
143
185
  #
@@ -150,7 +192,7 @@ module Capybara
150
192
  # @return [String] A snapshot of the DOM of the current document, as it looks right now (potentially modified by JavaScript).
151
193
  #
152
194
  def html
153
- driver.html
195
+ driver.html || ''
154
196
  end
155
197
  alias_method :body, :html
156
198
  alias_method :source, :html
@@ -160,8 +202,14 @@ module Capybara
160
202
  # @return [String] Path of the current page, without any domain information
161
203
  #
162
204
  def current_path
163
- path = URI.parse(current_url).path
164
- path if path and not path.empty?
205
+ # Addressable parsing is more lenient than URI
206
+ uri = ::Addressable::URI.parse(current_url)
207
+
208
+ # Addressable doesn't support opaque URIs - we want nil here
209
+ return nil if uri&.scheme == 'about'
210
+
211
+ path = uri&.path
212
+ path unless path&.empty?
165
213
  end
166
214
 
167
215
  ##
@@ -191,13 +239,13 @@ module Capybara
191
239
  #
192
240
  # For drivers which can run against an external application, such as the selenium driver
193
241
  # giving an absolute URL will navigate to that page. This allows testing applications
194
- # running on remote servers. For these drivers, setting {Capybara.app_host} will make the
242
+ # running on remote servers. For these drivers, setting {Capybara.configure app_host} will make the
195
243
  # remote server the default. For example:
196
244
  #
197
245
  # Capybara.app_host = 'http://google.com'
198
246
  # session.visit('/') # visits the google homepage
199
247
  #
200
- # If {Capybara.always_include_port} is set to true and this session is running against
248
+ # If {Capybara.configure always_include_port} is set to `true` and this session is running against
201
249
  # a rack application, then the port that the rack application is running on will automatically
202
250
  # be inserted into the URL. Supposing the app is running on port `4567`, doing something like:
203
251
  #
@@ -211,28 +259,34 @@ module Capybara
211
259
  raise_server_error!
212
260
  @touched = true
213
261
 
214
- visit_uri = URI.parse(visit_uri.to_s)
262
+ visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
263
+ base_uri = ::Addressable::URI.parse(config.app_host || server_url)
215
264
 
216
- uri_base = if @server
217
- visit_uri.port = @server.port if Capybara.always_include_port && (visit_uri.port == visit_uri.default_port)
218
- URI.parse(Capybara.app_host || "http://#{@server.host}:#{@server.port}")
219
- else
220
- Capybara.app_host && URI.parse(Capybara.app_host)
221
- end
265
+ if base_uri && [nil, 'http', 'https'].include?(visit_uri.scheme)
266
+ if visit_uri.relative?
267
+ visit_uri_parts = visit_uri.to_hash.compact
222
268
 
223
- # TODO - this is only for compatability with previous 2.x behavior that concatenated
224
- # Capybara.app_host and a "relative" path - Consider removing in 3.0
225
- # @abotalov brought up a good point about this behavior potentially being useful to people
226
- # deploying to a subdirectory and/or single page apps where only the url fragment changes
227
- if visit_uri.scheme.nil? && uri_base
228
- visit_uri.path = uri_base.path + visit_uri.path
229
- end
269
+ # Useful to people deploying to a subdirectory
270
+ # and/or single page apps where only the url fragment changes
271
+ visit_uri_parts[:path] = base_uri.path + visit_uri.path
230
272
 
231
- visit_uri = uri_base.merge(visit_uri) unless uri_base.nil?
273
+ visit_uri = base_uri.merge(visit_uri_parts)
274
+ end
275
+ adjust_server_port(visit_uri)
276
+ end
232
277
 
233
278
  driver.visit(visit_uri.to_s)
234
279
  end
235
280
 
281
+ ##
282
+ #
283
+ # Refresh the page.
284
+ #
285
+ def refresh
286
+ raise_server_error!
287
+ driver.refresh
288
+ end
289
+
236
290
  ##
237
291
  #
238
292
  # Move back a single entry in the browser's history.
@@ -249,32 +303,40 @@ module Capybara
249
303
  driver.go_forward
250
304
  end
251
305
 
306
+ ##
307
+ # @!method send_keys
308
+ # @see Capybara::Node::Element#send_keys
309
+ #
310
+ def send_keys(*args, **kw_args)
311
+ driver.send_keys(*args, **kw_args)
312
+ end
313
+
252
314
  ##
253
315
  #
254
- # Executes the given block within the context of a node. `within` takes the
255
- # same options as `find`, as well as a block. For the duration of the
316
+ # Executes the given block within the context of a node. {#within} takes the
317
+ # same options as {Capybara::Node::Finders#find #find}, as well as a block. For the duration of the
256
318
  # block, any command to Capybara will be handled as though it were scoped
257
319
  # to the given element.
258
320
  #
259
- # within(:xpath, '//div[@id="delivery-address"]') do
260
- # fill_in('Street', :with => '12 Main Street')
321
+ # within(:xpath, './/div[@id="delivery-address"]') do
322
+ # fill_in('Street', with: '12 Main Street')
261
323
  # end
262
324
  #
263
- # Just as with `find`, if multiple elements match the selector given to
264
- # `within`, an error will be raised, and just as with `find`, this
325
+ # Just as with `#find`, if multiple elements match the selector given to
326
+ # {#within}, an error will be raised, and just as with `#find`, this
265
327
  # behaviour can be controlled through the `:match` and `:exact` options.
266
328
  #
267
329
  # It is possible to omit the first parameter, in that case, the selector is
268
- # assumed to be of the type set in Capybara.default_selector.
330
+ # assumed to be of the type set in {Capybara.configure default_selector}.
269
331
  #
270
332
  # within('div#delivery-address') do
271
- # fill_in('Street', :with => '12 Main Street')
333
+ # fill_in('Street', with: '12 Main Street')
272
334
  # end
273
335
  #
274
- # Note that a lot of uses of `within` can be replaced more succinctly with
336
+ # Note that a lot of uses of {#within} can be replaced more succinctly with
275
337
  # chaining:
276
338
  #
277
- # find('div#delivery-address').fill_in('Street', :with => '12 Main Street')
339
+ # find('div#delivery-address').fill_in('Street', with: '12 Main Street')
278
340
  #
279
341
  # @overload within(*find_args)
280
342
  # @param (see Capybara::Node::Finders#all)
@@ -284,15 +346,16 @@ module Capybara
284
346
  #
285
347
  # @raise [Capybara::ElementNotFound] If the scope can't be found before time expires
286
348
  #
287
- def within(*args)
288
- new_scope = if args.first.is_a?(Capybara::Node::Base) then args.first else find(*args) end
349
+ def within(*args, **kw_args)
350
+ new_scope = args.first.respond_to?(:to_capybara_node) ? args.first.to_capybara_node : find(*args, **kw_args)
289
351
  begin
290
352
  scopes.push(new_scope)
291
- yield
353
+ yield if block_given?
292
354
  ensure
293
355
  scopes.pop
294
356
  end
295
357
  end
358
+ alias_method :within_element, :within
296
359
 
297
360
  ##
298
361
  #
@@ -300,10 +363,8 @@ module Capybara
300
363
  #
301
364
  # @param [String] locator Id or legend of the fieldset
302
365
  #
303
- def within_fieldset(locator)
304
- within :fieldset, locator do
305
- yield
306
- end
366
+ def within_fieldset(locator, &block)
367
+ within(:fieldset, locator, &block)
307
368
  end
308
369
 
309
370
  ##
@@ -312,29 +373,72 @@ module Capybara
312
373
  #
313
374
  # @param [String] locator Id or caption of the table
314
375
  #
315
- def within_table(locator)
316
- within :table, locator do
317
- yield
376
+ def within_table(locator, &block)
377
+ within(:table, locator, &block)
378
+ end
379
+
380
+ ##
381
+ #
382
+ # Switch to the given frame.
383
+ #
384
+ # If you use this method you are responsible for making sure you switch back to the parent frame when done in the frame changed to.
385
+ # {#within_frame} is preferred over this method and should be used when possible.
386
+ # May not be supported by all drivers.
387
+ #
388
+ # @overload switch_to_frame(element)
389
+ # @param [Capybara::Node::Element] element iframe/frame element to switch to
390
+ # @overload switch_to_frame(location)
391
+ # @param [Symbol] location relative location of the frame to switch to
392
+ # * :parent - the parent frame
393
+ # * :top - the top level document
394
+ #
395
+ def switch_to_frame(frame)
396
+ case frame
397
+ when Capybara::Node::Element
398
+ driver.switch_to_frame(frame)
399
+ scopes.push(:frame)
400
+ when :parent
401
+ if scopes.last != :frame
402
+ raise Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's "\
403
+ '`within` block.'
404
+ end
405
+ scopes.pop
406
+ driver.switch_to_frame(:parent)
407
+ when :top
408
+ idx = scopes.index(:frame)
409
+ top_level_scopes = [:frame, nil]
410
+ if idx
411
+ if scopes.slice(idx..-1).any? { |scope| !top_level_scopes.include?(scope) }
412
+ raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's "\
413
+ '`within` block.'
414
+ end
415
+ scopes.slice!(idx..-1)
416
+ driver.switch_to_frame(:top)
417
+ end
418
+ else
419
+ raise ArgumentError, 'You must provide a frame element, :parent, or :top when calling switch_to_frame'
318
420
  end
319
421
  end
320
422
 
321
423
  ##
322
424
  #
323
- # Execute the given block within the given iframe using given frame name or index.
324
- # May be supported by not all drivers. Drivers that support it, may provide additional options.
425
+ # Execute the given block within the given iframe using given frame, frame name/id or index.
426
+ # May not be supported by all drivers.
325
427
  #
428
+ # @overload within_frame(element)
429
+ # @param [Capybara::Node::Element] frame element
430
+ # @overload within_frame([kind = :frame], locator, **options)
431
+ # @param [Symbol] kind Optional selector type (:frame, :css, :xpath, etc.) - Defaults to :frame
432
+ # @param [String] locator The locator for the given selector kind. For :frame this is the name/id of a frame/iframe element
326
433
  # @overload within_frame(index)
327
- # @param [Integer] index index of a frame
328
- # @overload within_frame(name)
329
- # @param [String] name name of a frame
330
- #
331
- def within_frame(frame_handle)
332
- scopes.push(nil)
333
- driver.within_frame(frame_handle) do
334
- yield
434
+ # @param [Integer] index index of a frame (0 based)
435
+ def within_frame(*args, **kw_args)
436
+ switch_to_frame(_find_frame(*args, **kw_args))
437
+ begin
438
+ yield if block_given?
439
+ ensure
440
+ switch_to_frame(:parent)
335
441
  end
336
- ensure
337
- scopes.pop
338
442
  end
339
443
 
340
444
  ##
@@ -358,70 +462,50 @@ module Capybara
358
462
  end
359
463
 
360
464
  ##
361
- # Open new window.
362
- # Current window doesn't change as the result of this call.
465
+ # Open a new window.
466
+ # The current window doesn't change as the result of this call.
363
467
  # It should be switched to explicitly.
364
468
  #
365
469
  # @return [Capybara::Window] window that has been opened
366
470
  #
367
- def open_new_window
471
+ def open_new_window(kind = :tab)
368
472
  window_opened_by do
369
- driver.open_new_window
473
+ if driver.method(:open_new_window).arity.zero?
474
+ driver.open_new_window
475
+ else
476
+ driver.open_new_window(kind)
477
+ end
370
478
  end
371
479
  end
372
480
 
373
481
  ##
482
+ # Switch to the given window.
483
+ #
374
484
  # @overload switch_to_window(&block)
375
485
  # Switches to the first window for which given block returns a value other than false or nil.
376
- # If window that matches block can't be found, the window will be switched back and `WindowError` will be raised.
486
+ # If window that matches block can't be found, the window will be switched back and {Capybara::WindowError} will be raised.
377
487
  # @example
378
488
  # window = switch_to_window { title == 'Page title' }
379
489
  # @raise [Capybara::WindowError] if no window matches given block
380
490
  # @overload switch_to_window(window)
381
491
  # @param window [Capybara::Window] window that should be switched to
382
- # @raise [Capybara::Driver::Base#no_such_window_error] if unexistent (e.g. closed) window was passed
492
+ # @raise [Capybara::Driver::Base#no_such_window_error] if nonexistent (e.g. closed) window was passed
383
493
  #
384
494
  # @return [Capybara::Window] window that has been switched to
385
- # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
386
- # `within_frame` or `within_window` methods
495
+ # @raise [Capybara::ScopeError] if this method is invoked inside {#within} or
496
+ # {#within_frame} methods
387
497
  # @raise [ArgumentError] if both or neither arguments were provided
388
498
  #
389
- def switch_to_window(window = nil, options= {})
390
- options, window = window, nil if window.is_a? Hash
391
-
392
- block_given = block_given?
393
- if window && block_given
394
- raise ArgumentError, "`switch_to_window` can take either a block or a window, not both"
395
- elsif !window && !block_given
396
- raise ArgumentError, "`switch_to_window`: either window or block should be provided"
397
- elsif scopes.size > 1
398
- raise Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from "\
399
- "`within`'s, `within_frame`'s' or `within_window`'s' block."
400
- end
499
+ def switch_to_window(window = nil, **options, &window_locator)
500
+ raise ArgumentError, '`switch_to_window` can take either a block or a window, not both' if window && window_locator
501
+ raise ArgumentError, '`switch_to_window`: either window or block should be provided' if !window && !window_locator
401
502
 
402
- if window
403
- driver.switch_to_window(window.handle)
404
- window
405
- else
406
- wait_time = Capybara::Queries::SelectorQuery.new(options).wait
407
- document.synchronize(wait_time, errors: [Capybara::WindowError]) do
408
- original_window_handle = driver.current_window_handle
409
- begin
410
- driver.window_handles.each do |handle|
411
- driver.switch_to_window handle
412
- if yield
413
- return Window.new(self, handle)
414
- end
415
- end
416
- rescue => e
417
- driver.switch_to_window(original_window_handle)
418
- raise e
419
- else
420
- driver.switch_to_window(original_window_handle)
421
- raise Capybara::WindowError, "Could not find a window matching block/lambda"
422
- end
423
- end
503
+ unless scopes.last.nil?
504
+ raise Capybara::ScopeError, '`switch_to_window` is not supposed to be invoked from '\
505
+ '`within` or `within_frame` blocks.'
424
506
  end
507
+
508
+ _switch_to_window(window, **options, &window_locator)
425
509
  end
426
510
 
427
511
  ##
@@ -429,58 +513,42 @@ module Capybara
429
513
  #
430
514
  # 1. Switches to the given window (it can be located by window instance/lambda/string).
431
515
  # 2. Executes the given block (within window located at previous step).
432
- # 3. Switches back (this step will be invoked even if exception will happen at second step)
516
+ # 3. Switches back (this step will be invoked even if an exception occurs at the second step).
433
517
  #
434
518
  # @overload within_window(window) { do_something }
435
- # @param window [Capybara::Window] instance of `Capybara::Window` class
519
+ # @param window [Capybara::Window] instance of {Capybara::Window} class
436
520
  # that will be switched to
437
- # @raise [driver#no_such_window_error] if unexistent (e.g. closed) window was passed
521
+ # @raise [driver#no_such_window_error] if nonexistent (e.g. closed) window was passed
438
522
  # @overload within_window(proc_or_lambda) { do_something }
439
- # @param lambda [Proc] lambda. First window for which lambda
523
+ # @param lambda [Proc] First window for which lambda
440
524
  # returns a value other than false or nil will be switched to.
441
525
  # @example
442
526
  # within_window(->{ page.title == 'Page title' }) { click_button 'Submit' }
443
527
  # @raise [Capybara::WindowError] if no window matching lambda was found
444
- # @overload within_window(string) { do_something }
445
- # @deprecated Pass window or lambda instead
446
- # @param [String] handle, name, url or title of the window
447
528
  #
448
- # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
449
- # `within_frame` or `within_window` methods
529
+ # @raise [Capybara::ScopeError] if this method is invoked inside {#within_frame} method
450
530
  # @return value returned by the block
451
531
  #
452
- def within_window(window_or_handle)
453
- if window_or_handle.instance_of?(Capybara::Window)
454
- original = current_window
455
- switch_to_window(window_or_handle) unless original == window_or_handle
456
- scopes << nil
457
- begin
458
- yield
459
- ensure
460
- @scopes.pop
461
- switch_to_window(original) unless original == window_or_handle
462
- end
463
- elsif window_or_handle.is_a?(Proc)
464
- original = current_window
465
- switch_to_window { window_or_handle.call }
466
- scopes << nil
467
- begin
468
- yield
469
- ensure
470
- @scopes.pop
471
- switch_to_window(original)
532
+ def within_window(window_or_proc)
533
+ original = current_window
534
+ scopes << nil
535
+ begin
536
+ case window_or_proc
537
+ when Capybara::Window
538
+ _switch_to_window(window_or_proc) unless original == window_or_proc
539
+ when Proc
540
+ _switch_to_window { window_or_proc.call }
541
+ else
542
+ raise ArgumentError, '`#within_window` requires a `Capybara::Window` instance or a lambda'
472
543
  end
473
- else
474
- offending_line = caller.first
475
- file_line = offending_line.match(/^(.+?):(\d+)/)[0]
476
- warn "DEPRECATION WARNING: Passing string argument to #within_window is deprecated. "\
477
- "Pass window object or lambda. (called from #{file_line})"
544
+
478
545
  begin
479
- scopes << nil
480
- driver.within_window(window_or_handle) { yield }
546
+ yield if block_given?
481
547
  ensure
482
- @scopes.pop
548
+ _switch_to_window(original) unless original == window_or_proc
483
549
  end
550
+ ensure
551
+ scopes.pop
484
552
  end
485
553
  end
486
554
 
@@ -488,23 +556,23 @@ module Capybara
488
556
  # Get the window that has been opened by the passed block.
489
557
  # It will wait for it to be opened (in the same way as other Capybara methods wait).
490
558
  # It's better to use this method than `windows.last`
491
- # {https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10 as order of windows isn't defined in some drivers}
559
+ # {https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10 as order of windows isn't defined in some drivers}.
492
560
  #
493
- # @param options [Hash]
494
- # @option options [Numeric] :wait (Capybara.default_max_wait_time) maximum wait time
495
- # @return [Capybara::Window] the window that has been opened within a block
496
- # @raise [Capybara::WindowError] if block passed to window hasn't opened window
497
- # or opened more than one window
561
+ # @overload window_opened_by(**options, &block)
562
+ # @param options [Hash]
563
+ # @option options [Numeric] :wait maximum wait time. Defaults to {Capybara.configure default_max_wait_time}
564
+ # @return [Capybara::Window] the window that has been opened within a block
565
+ # @raise [Capybara::WindowError] if block passed to window hasn't opened window
566
+ # or opened more than one window
498
567
  #
499
- def window_opened_by(options = {}, &block)
568
+ def window_opened_by(**options)
500
569
  old_handles = driver.window_handles
501
- block.call
570
+ yield
502
571
 
503
- wait_time = Capybara::Queries::SelectorQuery.new(options).wait
504
- document.synchronize(wait_time, errors: [Capybara::WindowError]) do
572
+ synchronize_windows(options) do
505
573
  opened_handles = (driver.window_handles - old_handles)
506
574
  if opened_handles.size != 1
507
- raise Capybara::WindowError, "block passed to #window_opened_by "\
575
+ raise Capybara::WindowError, 'block passed to #window_opened_by '\
508
576
  "opened #{opened_handles.size} windows instead of 1"
509
577
  end
510
578
  Window.new(self, opened_handles.first)
@@ -514,28 +582,45 @@ module Capybara
514
582
  ##
515
583
  #
516
584
  # Execute the given script, not returning a result. This is useful for scripts that return
517
- # complex objects, such as jQuery statements. +execute_script+ should be used over
518
- # +evaluate_script+ whenever possible.
585
+ # complex objects, such as jQuery statements. {#execute_script} should be used over
586
+ # {#evaluate_script} whenever possible.
519
587
  #
520
588
  # @param [String] script A string of JavaScript to execute
589
+ # @param args Optional arguments that will be passed to the script. Driver support for this is optional and types of objects supported may differ between drivers
521
590
  #
522
- def execute_script(script)
591
+ def execute_script(script, *args)
523
592
  @touched = true
524
- driver.execute_script(script)
593
+ driver.execute_script(script, *driver_args(args))
525
594
  end
526
595
 
527
596
  ##
528
597
  #
529
598
  # Evaluate the given JavaScript and return the result. Be careful when using this with
530
- # scripts that return complex objects, such as jQuery statements. +execute_script+ might
599
+ # scripts that return complex objects, such as jQuery statements. {#execute_script} might
531
600
  # be a better alternative.
532
601
  #
533
602
  # @param [String] script A string of JavaScript to evaluate
603
+ # @param args Optional arguments that will be passed to the script
604
+ # @return [Object] The result of the evaluated JavaScript (may be driver specific)
605
+ #
606
+ def evaluate_script(script, *args)
607
+ @touched = true
608
+ result = driver.evaluate_script(script.strip, *driver_args(args))
609
+ element_script_result(result)
610
+ end
611
+
612
+ ##
613
+ #
614
+ # Evaluate the given JavaScript and obtain the result from a callback function which will be passed as the last argument to the script.
615
+ #
616
+ # @param [String] script A string of JavaScript to evaluate
617
+ # @param args Optional arguments that will be passed to the script
534
618
  # @return [Object] The result of the evaluated JavaScript (may be driver specific)
535
619
  #
536
- def evaluate_script(script)
620
+ def evaluate_async_script(script, *args)
537
621
  @touched = true
538
- driver.evaluate_script(script)
622
+ result = driver.evaluate_async_script(script, *driver_args(args))
623
+ element_script_result(result)
539
624
  end
540
625
 
541
626
  ##
@@ -543,19 +628,23 @@ module Capybara
543
628
  # Execute the block, accepting a alert.
544
629
  #
545
630
  # @!macro modal_params
546
- # @overload $0(text, options = {}, &blk)
547
- # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any modal is matched
548
- # @overload $0(options = {}, &blk)
549
- # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time to wait for the modal to appear after executing the block.
631
+ # Expects a block whose actions will trigger the display modal to appear.
632
+ # @example
633
+ # $0 do
634
+ # click_link('link that triggers appearance of system modal')
635
+ # end
636
+ # @overload $0(text, **options, &blk)
637
+ # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any modal is matched.
638
+ # @option options [Numeric] :wait Maximum time to wait for the modal to appear after executing the block. Defaults to {Capybara.configure default_max_wait_time}.
639
+ # @yield Block whose actions will trigger the system modal
640
+ # @overload $0(**options, &blk)
641
+ # @option options [Numeric] :wait Maximum time to wait for the modal to appear after executing the block. Defaults to {Capybara.configure default_max_wait_time}.
642
+ # @yield Block whose actions will trigger the system modal
550
643
  # @return [String] the message shown in the modal
551
644
  # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
552
645
  #
553
- def accept_alert(text_or_options=nil, options={}, &blk)
554
- text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
555
- options[:text] ||= text_or_options unless text_or_options.nil?
556
- options[:wait] ||= Capybara.default_max_wait_time
557
-
558
- driver.accept_modal(:alert, options, &blk)
646
+ def accept_alert(text = nil, **options, &blk)
647
+ accept_modal(:alert, text, options, &blk)
559
648
  end
560
649
 
561
650
  ##
@@ -564,12 +653,8 @@ module Capybara
564
653
  #
565
654
  # @macro modal_params
566
655
  #
567
- def accept_confirm(text_or_options=nil, options={}, &blk)
568
- text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
569
- options[:text] ||= text_or_options unless text_or_options.nil?
570
- options[:wait] ||= Capybara.default_max_wait_time
571
-
572
- driver.accept_modal(:confirm, options, &blk)
656
+ def accept_confirm(text = nil, **options, &blk)
657
+ accept_modal(:confirm, text, options, &blk)
573
658
  end
574
659
 
575
660
  ##
@@ -578,12 +663,8 @@ module Capybara
578
663
  #
579
664
  # @macro modal_params
580
665
  #
581
- def dismiss_confirm(text_or_options=nil, options={}, &blk)
582
- text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
583
- options[:text] ||= text_or_options unless text_or_options.nil?
584
- options[:wait] ||= Capybara.default_max_wait_time
585
-
586
- driver.dismiss_modal(:confirm, options, &blk)
666
+ def dismiss_confirm(text = nil, **options, &blk)
667
+ dismiss_modal(:confirm, text, options, &blk)
587
668
  end
588
669
 
589
670
  ##
@@ -593,12 +674,8 @@ module Capybara
593
674
  # @macro modal_params
594
675
  # @option options [String] :with Response to provide to the prompt
595
676
  #
596
- def accept_prompt(text_or_options=nil, options={}, &blk)
597
- text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
598
- options[:text] ||= text_or_options unless text_or_options.nil?
599
- options[:wait] ||= Capybara.default_max_wait_time
600
-
601
- driver.accept_modal(:prompt, options, &blk)
677
+ def accept_prompt(text = nil, **options, &blk)
678
+ accept_modal(:prompt, text, options, &blk)
602
679
  end
603
680
 
604
681
  ##
@@ -607,86 +684,70 @@ module Capybara
607
684
  #
608
685
  # @macro modal_params
609
686
  #
610
- def dismiss_prompt(text_or_options=nil, options={}, &blk)
611
- text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
612
- options[:text] ||= text_or_options unless text_or_options.nil?
613
- options[:wait] ||= Capybara.default_max_wait_time
614
-
615
- driver.dismiss_modal(:prompt, options, &blk)
687
+ def dismiss_prompt(text = nil, **options, &blk)
688
+ dismiss_modal(:prompt, text, options, &blk)
616
689
  end
617
690
 
618
691
  ##
619
692
  #
620
- # Save a snapshot of the page. If `Capybara.asset_host` is set it will inject `base` tag
621
- # pointing to `asset_host`.
693
+ # Save a snapshot of the page. If {Capybara.configure asset_host} is set it will inject `base` tag
694
+ # pointing to {Capybara.configure asset_host}.
622
695
  #
623
- # If invoked without arguments it will save file to `Capybara.save_path`
624
- # and file will be given randomly generated filename. If invoked with a relative path
625
- # the path will be relative to `Capybara.save_path`, which is different from
626
- # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
627
- # relative to Dir.pwd
696
+ # If invoked without arguments it will save file to {Capybara.configure save_path}
697
+ # and file will be given randomly generated filename. If invoked with a relative path
698
+ # the path will be relative to {Capybara.configure save_path}.
628
699
  #
629
700
  # @param [String] path the path to where it should be saved
630
701
  # @return [String] the path to which the file was saved
631
702
  #
632
703
  def save_page(path = nil)
633
- path = prepare_path(path, 'html')
634
- File.write(path, Capybara::Helpers.inject_asset_host(body), mode: 'wb')
635
- path
704
+ prepare_path(path, 'html').tap do |p_path|
705
+ File.write(p_path, Capybara::Helpers.inject_asset_host(body, host: config.asset_host), mode: 'wb')
706
+ end
636
707
  end
637
708
 
638
709
  ##
639
710
  #
640
711
  # Save a snapshot of the page and open it in a browser for inspection.
641
712
  #
642
- # If invoked without arguments it will save file to `Capybara.save_path`
643
- # and file will be given randomly generated filename. If invoked with a relative path
644
- # the path will be relative to `Capybara.save_path`, which is different from
645
- # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
646
- # relative to Dir.pwd
713
+ # If invoked without arguments it will save file to {Capybara.configure save_path}
714
+ # and file will be given randomly generated filename. If invoked with a relative path
715
+ # the path will be relative to {Capybara.configure save_path}.
647
716
  #
648
717
  # @param [String] path the path to where it should be saved
649
718
  #
650
719
  def save_and_open_page(path = nil)
651
- path = save_page(path)
652
- open_file(path)
720
+ save_page(path).tap { |s_path| open_file(s_path) }
653
721
  end
654
722
 
655
723
  ##
656
724
  #
657
725
  # Save a screenshot of page.
658
726
  #
659
- # If invoked without arguments it will save file to `Capybara.save_path`
660
- # and file will be given randomly generated filename. If invoked with a relative path
661
- # the path will be relative to `Capybara.save_path`, which is different from
662
- # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
663
- # relative to Dir.pwd
727
+ # If invoked without arguments it will save file to {Capybara.configure save_path}
728
+ # and file will be given randomly generated filename. If invoked with a relative path
729
+ # the path will be relative to {Capybara.configure save_path}.
664
730
  #
665
731
  # @param [String] path the path to where it should be saved
666
732
  # @param [Hash] options a customizable set of options
667
733
  # @return [String] the path to which the file was saved
668
- def save_screenshot(path = nil, options = {})
669
- path = prepare_path(path, 'png')
670
- driver.save_screenshot(path, options)
671
- path
734
+ def save_screenshot(path = nil, **options)
735
+ prepare_path(path, 'png').tap { |p_path| driver.save_screenshot(p_path, **options) }
672
736
  end
673
737
 
674
738
  ##
675
739
  #
676
740
  # Save a screenshot of the page and open it for inspection.
677
741
  #
678
- # If invoked without arguments it will save file to `Capybara.save_path`
679
- # and file will be given randomly generated filename. If invoked with a relative path
680
- # the path will be relative to `Capybara.save_path`, which is different from
681
- # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
682
- # relative to Dir.pwd
742
+ # If invoked without arguments it will save file to {Capybara.configure save_path}
743
+ # and file will be given randomly generated filename. If invoked with a relative path
744
+ # the path will be relative to {Capybara.configure save_path}.
683
745
  #
684
746
  # @param [String] path the path to where it should be saved
685
747
  # @param [Hash] options a customizable set of options
686
748
  #
687
- def save_and_open_screenshot(path = nil, options = {})
688
- path = save_screenshot(path, options)
689
- open_file(path)
749
+ def save_and_open_screenshot(path = nil, **options)
750
+ save_screenshot(path, **options).tap { |s_path| open_file(s_path) }
690
751
  end
691
752
 
692
753
  def document
@@ -694,15 +755,32 @@ module Capybara
694
755
  end
695
756
 
696
757
  NODE_METHODS.each do |method|
697
- define_method method do |*args, &block|
698
- @touched = true
699
- current_scope.send(method, *args, &block)
758
+ if RUBY_VERSION >= '2.7'
759
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
760
+ def #{method}(...)
761
+ @touched = true
762
+ current_scope.#{method}(...)
763
+ end
764
+ METHOD
765
+ else
766
+ define_method method do |*args, &block|
767
+ @touched = true
768
+ current_scope.send(method, *args, &block)
769
+ end
700
770
  end
701
771
  end
702
772
 
703
773
  DOCUMENT_METHODS.each do |method|
704
- define_method method do |*args, &block|
705
- document.send(method, *args, &block)
774
+ if RUBY_VERSION >= '2.7'
775
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
776
+ def #{method}(...)
777
+ document.#{method}(...)
778
+ end
779
+ METHOD
780
+ else
781
+ define_method method do |*args, &block|
782
+ document.send(method, *args, &block)
783
+ end
706
784
  end
707
785
  end
708
786
 
@@ -711,38 +789,163 @@ module Capybara
711
789
  end
712
790
 
713
791
  def current_scope
714
- scopes.last || document
792
+ scope = scopes.last
793
+ [nil, :frame].include?(scope) ? document : scope
794
+ end
795
+
796
+ ##
797
+ #
798
+ # Yield a block using a specific maximum wait time.
799
+ #
800
+ def using_wait_time(seconds, &block)
801
+ if Capybara.threadsafe
802
+ begin
803
+ previous_wait_time = config.default_max_wait_time
804
+ config.default_max_wait_time = seconds
805
+ yield
806
+ ensure
807
+ config.default_max_wait_time = previous_wait_time
808
+ end
809
+ else
810
+ Capybara.using_wait_time(seconds, &block)
811
+ end
812
+ end
813
+
814
+ ##
815
+ #
816
+ # Accepts a block to set the configuration options if {Capybara.configure threadsafe} is `true`. Note that some options only have an effect
817
+ # if set at initialization time, so look at the configuration block that can be passed to the initializer too.
818
+ #
819
+ def configure
820
+ raise 'Session configuration is only supported when Capybara.threadsafe == true' unless Capybara.threadsafe
821
+
822
+ yield config
823
+ end
824
+
825
+ def self.instance_created?
826
+ @@instance_created
827
+ end
828
+
829
+ def config
830
+ @config ||= if Capybara.threadsafe
831
+ Capybara.session_options.dup
832
+ else
833
+ Capybara::ReadOnlySessionConfig.new(Capybara.session_options)
834
+ end
835
+ end
836
+
837
+ def server_url
838
+ @server&.base_url
715
839
  end
716
840
 
717
841
  private
718
842
 
843
+ @@instance_created = false # rubocop:disable Style/ClassVars
844
+
845
+ def driver_args(args)
846
+ args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg }
847
+ end
848
+
849
+ def accept_modal(type, text_or_options, options, &blk)
850
+ driver.accept_modal(type, **modal_options(text_or_options, **options), &blk)
851
+ end
852
+
853
+ def dismiss_modal(type, text_or_options, options, &blk)
854
+ driver.dismiss_modal(type, **modal_options(text_or_options, **options), &blk)
855
+ end
856
+
857
+ def modal_options(text = nil, **options)
858
+ options[:text] ||= text unless text.nil?
859
+ options[:wait] ||= config.default_max_wait_time
860
+ options
861
+ end
862
+
719
863
  def open_file(path)
720
- begin
721
- require "launchy"
722
- Launchy.open(path)
723
- rescue LoadError
724
- warn "File saved to #{path}."
725
- warn "Please install the launchy gem to open the file automatically."
726
- end
864
+ require 'launchy'
865
+ Launchy.open(path)
866
+ rescue LoadError
867
+ warn "File saved to #{path}.\nPlease install the launchy gem to open the file automatically."
727
868
  end
728
869
 
729
870
  def prepare_path(path, extension)
730
- if Capybara.save_path || Capybara.save_and_open_page_path.nil?
731
- path = File.expand_path(path || default_fn(extension), Capybara.save_path)
732
- else
733
- path = File.expand_path(default_fn(extension), Capybara.save_and_open_page_path) if path.nil?
871
+ File.expand_path(path || default_fn(extension), config.save_path).tap do |p_path|
872
+ FileUtils.mkdir_p(File.dirname(p_path))
734
873
  end
735
- FileUtils.mkdir_p(File.dirname(path))
736
- path
737
874
  end
738
875
 
739
876
  def default_fn(extension)
740
- timestamp = Time.new.strftime("%Y%m%d%H%M%S")
741
- path = "capybara-#{timestamp}#{rand(10**10)}.#{extension}"
877
+ timestamp = Time.new.strftime('%Y%m%d%H%M%S')
878
+ "capybara-#{timestamp}#{rand(10**10)}.#{extension}"
742
879
  end
743
880
 
744
881
  def scopes
745
882
  @scopes ||= [nil]
746
883
  end
884
+
885
+ def element_script_result(arg)
886
+ case arg
887
+ when Array
888
+ arg.map { |subarg| element_script_result(subarg) }
889
+ when Hash
890
+ arg.transform_values! { |value| element_script_result(value) }
891
+ when Capybara::Driver::Node
892
+ Capybara::Node::Element.new(self, arg, nil, nil)
893
+ else
894
+ arg
895
+ end
896
+ end
897
+
898
+ def adjust_server_port(uri)
899
+ uri.port ||= @server.port if @server && config.always_include_port
900
+ end
901
+
902
+ def _find_frame(*args, **kw_args)
903
+ case args[0]
904
+ when Capybara::Node::Element
905
+ args[0]
906
+ when String, nil
907
+ find(:frame, *args, **kw_args)
908
+ when Symbol
909
+ find(*args, **kw_args)
910
+ when Integer
911
+ idx = args[0]
912
+ all(:frame, minimum: idx + 1)[idx]
913
+ else
914
+ raise TypeError
915
+ end
916
+ end
917
+
918
+ def _switch_to_window(window = nil, **options, &window_locator)
919
+ raise Capybara::ScopeError, 'Window cannot be switched inside a `within_frame` block' if scopes.include?(:frame)
920
+ raise Capybara::ScopeError, 'Window cannot be switched inside a `within` block' unless scopes.last.nil?
921
+
922
+ if window
923
+ driver.switch_to_window(window.handle)
924
+ window
925
+ else
926
+ synchronize_windows(options) do
927
+ original_window_handle = driver.current_window_handle
928
+ begin
929
+ _switch_to_window_by_locator(&window_locator)
930
+ rescue StandardError
931
+ driver.switch_to_window(original_window_handle)
932
+ raise
933
+ end
934
+ end
935
+ end
936
+ end
937
+
938
+ def _switch_to_window_by_locator
939
+ driver.window_handles.each do |handle|
940
+ driver.switch_to_window handle
941
+ return Window.new(self, handle) if yield
942
+ end
943
+ raise Capybara::WindowError, 'Could not find a window matching block/lambda'
944
+ end
945
+
946
+ def synchronize_windows(options, &block)
947
+ wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
948
+ document.synchronize(wait_time, errors: [Capybara::WindowError], &block)
949
+ end
747
950
  end
748
951
  end