capybara 2.7.0 → 3.35.3

Sign up to get free protection for your applications and to get access to all the features.
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