capybara 3.32.2

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