capybara 3.32.2

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