capybara 3.13.2 → 3.40.0

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 (260) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +587 -16
  4. data/README.md +240 -90
  5. data/lib/capybara/config.rb +24 -11
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +8 -0
  8. data/lib/capybara/driver/node.rb +20 -4
  9. data/lib/capybara/dsl.rb +5 -3
  10. data/lib/capybara/helpers.rb +25 -4
  11. data/lib/capybara/minitest/spec.rb +174 -90
  12. data/lib/capybara/minitest.rb +256 -142
  13. data/lib/capybara/node/actions.rb +123 -77
  14. data/lib/capybara/node/base.rb +20 -12
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +3 -3
  17. data/lib/capybara/node/element.rb +223 -117
  18. data/lib/capybara/node/finders.rb +81 -71
  19. data/lib/capybara/node/matchers.rb +271 -134
  20. data/lib/capybara/node/simple.rb +18 -5
  21. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +8 -9
  24. data/lib/capybara/queries/base_query.rb +3 -2
  25. data/lib/capybara/queries/current_path_query.rb +15 -5
  26. data/lib/capybara/queries/selector_query.rb +364 -54
  27. data/lib/capybara/queries/sibling_query.rb +8 -6
  28. data/lib/capybara/queries/style_query.rb +2 -2
  29. data/lib/capybara/queries/text_query.rb +13 -1
  30. data/lib/capybara/queries/title_query.rb +1 -1
  31. data/lib/capybara/rack_test/browser.rb +76 -11
  32. data/lib/capybara/rack_test/driver.rb +10 -5
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +31 -9
  35. data/lib/capybara/rack_test/node.rb +74 -23
  36. data/lib/capybara/registration_container.rb +41 -0
  37. data/lib/capybara/registrations/drivers.rb +42 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  39. data/lib/capybara/registrations/servers.rb +66 -0
  40. data/lib/capybara/result.rb +44 -20
  41. data/lib/capybara/rspec/matcher_proxies.rb +13 -11
  42. data/lib/capybara/rspec/matchers/base.rb +31 -16
  43. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  44. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  45. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  46. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  47. data/lib/capybara/rspec/matchers/have_selector.rb +21 -21
  48. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  49. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  50. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  51. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  52. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  53. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  54. data/lib/capybara/rspec/matchers.rb +111 -68
  55. data/lib/capybara/rspec.rb +2 -0
  56. data/lib/capybara/selector/builders/css_builder.rb +11 -7
  57. data/lib/capybara/selector/builders/xpath_builder.rb +5 -3
  58. data/lib/capybara/selector/css.rb +11 -9
  59. data/lib/capybara/selector/definition/button.rb +68 -0
  60. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  61. data/lib/capybara/selector/definition/css.rb +10 -0
  62. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  63. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  64. data/lib/capybara/selector/definition/element.rb +28 -0
  65. data/lib/capybara/selector/definition/field.rb +40 -0
  66. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  67. data/lib/capybara/selector/definition/file_field.rb +13 -0
  68. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  69. data/lib/capybara/selector/definition/frame.rb +17 -0
  70. data/lib/capybara/selector/definition/id.rb +6 -0
  71. data/lib/capybara/selector/definition/label.rb +62 -0
  72. data/lib/capybara/selector/definition/link.rb +55 -0
  73. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  74. data/lib/capybara/selector/definition/option.rb +27 -0
  75. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  76. data/lib/capybara/selector/definition/select.rb +81 -0
  77. data/lib/capybara/selector/definition/table.rb +109 -0
  78. data/lib/capybara/selector/definition/table_row.rb +21 -0
  79. data/lib/capybara/selector/definition/xpath.rb +5 -0
  80. data/lib/capybara/selector/definition.rb +280 -0
  81. data/lib/capybara/selector/filter_set.rb +19 -18
  82. data/lib/capybara/selector/filters/base.rb +11 -2
  83. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  84. data/lib/capybara/selector/regexp_disassembler.rb +11 -7
  85. data/lib/capybara/selector/selector.rb +50 -440
  86. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  87. data/lib/capybara/selector.rb +473 -482
  88. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  89. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  90. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  91. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  92. data/lib/capybara/selenium/driver.rb +174 -62
  93. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +74 -18
  94. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  95. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +37 -3
  96. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  97. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  98. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  99. data/lib/capybara/selenium/extensions/find.rb +68 -45
  100. data/lib/capybara/selenium/extensions/html5_drag.rb +192 -22
  101. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  102. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  103. data/lib/capybara/selenium/node.rb +268 -72
  104. data/lib/capybara/selenium/nodes/chrome_node.rb +105 -9
  105. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  106. data/lib/capybara/selenium/nodes/firefox_node.rb +51 -61
  107. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  108. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  109. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  110. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  111. data/lib/capybara/selenium/patches/logs.rb +45 -0
  112. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  113. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  114. data/lib/capybara/server/animation_disabler.rb +43 -21
  115. data/lib/capybara/server/checker.rb +6 -2
  116. data/lib/capybara/server/middleware.rb +25 -13
  117. data/lib/capybara/server.rb +20 -4
  118. data/lib/capybara/session/config.rb +15 -11
  119. data/lib/capybara/session/matchers.rb +11 -11
  120. data/lib/capybara/session.rb +162 -131
  121. data/lib/capybara/spec/public/offset.js +6 -0
  122. data/lib/capybara/spec/public/test.js +105 -6
  123. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  124. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  125. data/lib/capybara/spec/session/all_spec.rb +89 -15
  126. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  127. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  128. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  129. data/lib/capybara/spec/session/attach_file_spec.rb +64 -31
  130. data/lib/capybara/spec/session/check_spec.rb +26 -4
  131. data/lib/capybara/spec/session/choose_spec.rb +14 -2
  132. data/lib/capybara/spec/session/click_button_spec.rb +109 -61
  133. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  134. data/lib/capybara/spec/session/click_link_spec.rb +23 -1
  135. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  136. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  137. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  138. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  139. data/lib/capybara/spec/session/fill_in_spec.rb +46 -5
  140. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  141. data/lib/capybara/spec/session/find_spec.rb +80 -7
  142. data/lib/capybara/spec/session/first_spec.rb +2 -2
  143. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  144. data/lib/capybara/spec/session/frame/within_frame_spec.rb +14 -1
  145. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  146. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  147. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  148. data/lib/capybara/spec/session/has_button_spec.rb +81 -0
  149. data/lib/capybara/spec/session/has_css_spec.rb +45 -8
  150. data/lib/capybara/spec/session/has_current_path_spec.rb +22 -7
  151. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  152. data/lib/capybara/spec/session/has_field_spec.rb +59 -1
  153. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  154. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  155. data/lib/capybara/spec/session/has_select_spec.rb +42 -8
  156. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  157. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  158. data/lib/capybara/spec/session/has_table_spec.rb +177 -0
  159. data/lib/capybara/spec/session/has_text_spec.rb +31 -3
  160. data/lib/capybara/spec/session/html_spec.rb +1 -1
  161. data/lib/capybara/spec/session/matches_style_spec.rb +6 -4
  162. data/lib/capybara/spec/session/node_spec.rb +697 -23
  163. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  164. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  165. data/lib/capybara/spec/session/reset_session_spec.rb +21 -7
  166. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  167. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  168. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  169. data/lib/capybara/spec/session/scroll_spec.rb +9 -7
  170. data/lib/capybara/spec/session/select_spec.rb +5 -10
  171. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  172. data/lib/capybara/spec/session/uncheck_spec.rb +3 -3
  173. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  174. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  175. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  176. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  178. data/lib/capybara/spec/session/window/window_spec.rb +54 -57
  179. data/lib/capybara/spec/session/window/windows_spec.rb +2 -2
  180. data/lib/capybara/spec/session/within_spec.rb +36 -0
  181. data/lib/capybara/spec/spec_helper.rb +30 -19
  182. data/lib/capybara/spec/test_app.rb +122 -34
  183. data/lib/capybara/spec/views/animated.erb +49 -0
  184. data/lib/capybara/spec/views/form.erb +86 -8
  185. data/lib/capybara/spec/views/frame_child.erb +3 -2
  186. data/lib/capybara/spec/views/frame_one.erb +2 -1
  187. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  188. data/lib/capybara/spec/views/frame_two.erb +1 -1
  189. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  190. data/lib/capybara/spec/views/layout.erb +10 -0
  191. data/lib/capybara/spec/views/obscured.erb +10 -10
  192. data/lib/capybara/spec/views/offset.erb +33 -0
  193. data/lib/capybara/spec/views/path.erb +2 -2
  194. data/lib/capybara/spec/views/popup_one.erb +1 -1
  195. data/lib/capybara/spec/views/popup_two.erb +1 -1
  196. data/lib/capybara/spec/views/react.erb +45 -0
  197. data/lib/capybara/spec/views/scroll.erb +2 -1
  198. data/lib/capybara/spec/views/spatial.erb +31 -0
  199. data/lib/capybara/spec/views/tables.erb +67 -0
  200. data/lib/capybara/spec/views/with_animation.erb +39 -4
  201. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  202. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  203. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  204. data/lib/capybara/spec/views/with_hover.erb +3 -2
  205. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  206. data/lib/capybara/spec/views/with_html.erb +34 -6
  207. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  208. data/lib/capybara/spec/views/with_js.erb +7 -4
  209. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  210. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  211. data/lib/capybara/spec/views/with_scope.erb +2 -2
  212. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  213. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  214. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  215. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  216. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  217. data/lib/capybara/spec/views/with_windows.erb +1 -1
  218. data/lib/capybara/spec/views/within_frames.erb +1 -1
  219. data/lib/capybara/version.rb +1 -1
  220. data/lib/capybara/window.rb +14 -18
  221. data/lib/capybara.rb +91 -126
  222. data/spec/basic_node_spec.rb +30 -16
  223. data/spec/capybara_spec.rb +40 -28
  224. data/spec/counter_spec.rb +35 -0
  225. data/spec/css_builder_spec.rb +3 -1
  226. data/spec/css_splitter_spec.rb +1 -1
  227. data/spec/dsl_spec.rb +33 -22
  228. data/spec/filter_set_spec.rb +5 -5
  229. data/spec/fixtures/selenium_driver_rspec_failure.rb +3 -3
  230. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  231. data/spec/minitest_spec.rb +24 -2
  232. data/spec/minitest_spec_spec.rb +60 -45
  233. data/spec/per_session_config_spec.rb +1 -1
  234. data/spec/rack_test_spec.rb +131 -98
  235. data/spec/regexp_dissassembler_spec.rb +53 -39
  236. data/spec/result_spec.rb +68 -66
  237. data/spec/rspec/features_spec.rb +9 -4
  238. data/spec/rspec/scenarios_spec.rb +6 -2
  239. data/spec/rspec/shared_spec_matchers.rb +137 -98
  240. data/spec/rspec_matchers_spec.rb +25 -0
  241. data/spec/rspec_spec.rb +23 -21
  242. data/spec/sauce_spec_chrome.rb +43 -0
  243. data/spec/selector_spec.rb +77 -21
  244. data/spec/selenium_spec_chrome.rb +141 -39
  245. data/spec/selenium_spec_chrome_remote.rb +32 -17
  246. data/spec/selenium_spec_edge.rb +36 -8
  247. data/spec/selenium_spec_firefox.rb +110 -68
  248. data/spec/selenium_spec_firefox_remote.rb +22 -15
  249. data/spec/selenium_spec_ie.rb +29 -22
  250. data/spec/selenium_spec_safari.rb +162 -0
  251. data/spec/server_spec.rb +153 -81
  252. data/spec/session_spec.rb +11 -4
  253. data/spec/shared_selenium_node.rb +79 -0
  254. data/spec/shared_selenium_session.rb +179 -74
  255. data/spec/spec_helper.rb +80 -5
  256. data/spec/whitespace_normalizer_spec.rb +54 -0
  257. data/spec/xpath_builder_spec.rb +3 -1
  258. metadata +218 -30
  259. data/lib/capybara/spec/session/source_spec.rb +0 -0
  260. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -11,33 +11,77 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
11
11
  clear_local_storage: nil,
12
12
  clear_session_storage: nil
13
13
  }.freeze
14
- SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout].freeze
14
+ SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout native_displayed].freeze
15
+ CAPS_VERSION = Gem::Requirement.new('< 4.8.0')
16
+
15
17
  attr_reader :app, :options
16
18
 
17
- def self.load_selenium
18
- require 'selenium-webdriver'
19
- warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
20
- rescue LoadError => err
21
- raise err if err.message !~ /selenium-webdriver/
19
+ class << self
20
+ attr_reader :selenium_webdriver_version
21
+
22
+ def load_selenium
23
+ require 'selenium-webdriver'
24
+ require 'capybara/selenium/patches/atoms'
25
+ require 'capybara/selenium/patches/is_displayed'
26
+
27
+ # Look up the version of `selenium-webdriver` to
28
+ # see if it's a version we support.
29
+ #
30
+ # By default, we use Gem.loaded_specs to determine
31
+ # the version number. However, in some cases, such
32
+ # as when loading `selenium-webdriver` outside of
33
+ # Rubygems, we fall back to referencing
34
+ # Selenium::WebDriver::VERSION. Ideally we'd
35
+ # use the constant in all cases, but earlier versions
36
+ # of `selenium-webdriver` didn't provide the constant.
37
+ @selenium_webdriver_version =
38
+ if Gem.loaded_specs['selenium-webdriver']
39
+ Gem.loaded_specs['selenium-webdriver'].version
40
+ else
41
+ Gem::Version.new(Selenium::WebDriver::VERSION)
42
+ end
43
+
44
+ unless Gem::Requirement.new('>= 4.8').satisfied_by? @selenium_webdriver_version
45
+ warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade to 4.8+."
46
+ end
47
+
48
+ @selenium_webdriver_version
49
+ rescue LoadError => e
50
+ raise e unless e.message.include?('selenium-webdriver')
51
+
52
+ raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
53
+ end
54
+
55
+ attr_reader :specializations
22
56
 
23
- raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
57
+ def register_specialization(browser_name, specialization)
58
+ @specializations ||= {}
59
+ @specializations[browser_name] = specialization
60
+ end
24
61
  end
25
62
 
26
63
  def browser
27
- @browser ||= begin
28
- if options[:timeout]
29
- options[:http_client] ||= Selenium::WebDriver::Remote::Http::Default.new(read_timeout: options[:timeout])
30
- end
31
- processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
32
- Selenium::WebDriver.for(options[:browser], processed_options).tap do |driver|
33
- specialize_driver(driver)
34
- setup_exit_handler
64
+ unless @browser
65
+ options[:http_client] ||= begin
66
+ require 'capybara/selenium/patches/persistent_client'
67
+ if options[:timeout]
68
+ ::Capybara::Selenium::PersistentClient.new(read_timeout: options[:timeout])
69
+ else
70
+ ::Capybara::Selenium::PersistentClient.new
71
+ end
35
72
  end
73
+ processed_options = options.except(*SPECIAL_OPTIONS)
74
+
75
+ @browser = Selenium::WebDriver.for(options[:browser], processed_options)
76
+
77
+ specialize_driver
78
+ setup_exit_handler
36
79
  end
37
80
  @browser
38
81
  end
39
82
 
40
83
  def initialize(app, **options)
84
+ super()
41
85
  self.class.load_selenium
42
86
  @app = app
43
87
  @browser = nil
@@ -65,6 +109,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
65
109
 
66
110
  def html
67
111
  browser.page_source
112
+ rescue Selenium::WebDriver::Error::JavascriptError => e
113
+ raise unless e.message.include?('documentElement is null')
68
114
  end
69
115
 
70
116
  def title
@@ -93,8 +139,17 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
93
139
  unwrap_script_result(result)
94
140
  end
95
141
 
96
- def save_screenshot(path, **_options)
97
- browser.save_screenshot(path)
142
+ def active_element
143
+ build_node(native_active_element)
144
+ end
145
+
146
+ def send_keys(*args)
147
+ # Should this call the specialized nodes rather than native???
148
+ native_active_element.send_keys(*args)
149
+ end
150
+
151
+ def save_screenshot(path, **options)
152
+ browser.save_screenshot(path, **options)
98
153
  end
99
154
 
100
155
  def reset!
@@ -110,7 +165,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
110
165
  navigated = true
111
166
  # Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
112
167
  wait_for_empty_page(timer)
113
- rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
168
+ rescue *unhandled_alert_errors
114
169
  # This error is thrown if an unhandled alert is on the page
115
170
  # Firefox appears to automatically dismiss this alert, chrome does not
116
171
  # We'll try to accept it
@@ -120,6 +175,18 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
120
175
  end
121
176
  end
122
177
 
178
+ def frame_obscured_at?(x:, y:)
179
+ frame = @frame_handles[current_window_handle].last
180
+ return false unless frame
181
+
182
+ switch_to_frame(:parent)
183
+ begin
184
+ frame.base.obscured?(x: x, y: y)
185
+ ensure
186
+ switch_to_frame(frame)
187
+ end
188
+ end
189
+
123
190
  def switch_to_frame(frame)
124
191
  handles = @frame_handles[current_window_handle]
125
192
  case frame
@@ -130,7 +197,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
130
197
  handles.pop
131
198
  browser.switch_to.parent_frame
132
199
  else
133
- handles << frame.native
200
+ handles << frame
134
201
  browser.switch_to.frame(frame.native)
135
202
  end
136
203
  end
@@ -177,7 +244,16 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
177
244
  browser.window_handles
178
245
  end
179
246
 
180
- def open_new_window
247
+ def open_new_window(kind = :tab)
248
+ if browser.switch_to.respond_to?(:new_window)
249
+ handle = current_window_handle
250
+ browser.switch_to.new_window(kind)
251
+ switch_to_window(handle)
252
+ else
253
+ browser.manage.new_window(kind)
254
+ end
255
+ rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
256
+ # If not supported by the driver or browser default to using JS
181
257
  browser.execute_script('window.open();')
182
258
  end
183
259
 
@@ -187,7 +263,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
187
263
 
188
264
  def accept_modal(_type, **options)
189
265
  yield if block_given?
190
- modal = find_modal(options)
266
+ modal = find_modal(**options)
191
267
 
192
268
  modal.send_keys options[:with] if options[:with]
193
269
 
@@ -198,7 +274,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
198
274
 
199
275
  def dismiss_modal(_type, **options)
200
276
  yield if block_given?
201
- modal = find_modal(options)
277
+ modal = find_modal(**options)
202
278
  message = modal.text
203
279
  modal.dismiss
204
280
  message
@@ -206,31 +282,32 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
206
282
 
207
283
  def quit
208
284
  @browser&.quit
209
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/HandleExceptions
285
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED,
286
+ Selenium::WebDriver::Error::InvalidSessionIdError
210
287
  # Browser must have already gone
211
- rescue Selenium::WebDriver::Error::UnknownError => err
212
- unless silenced_unknown_error_message?(err.message) # Most likely already gone
288
+ rescue Selenium::WebDriver::Error::UnknownError => e
289
+ unless silenced_unknown_error_message?(e.message) # Most likely already gone
213
290
  # probably already gone but not sure - so warn
214
- warn "Ignoring Selenium UnknownError during driver quit: #{err.message}"
291
+ warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
215
292
  end
216
293
  ensure
217
294
  @browser = nil
218
295
  end
219
296
 
220
297
  def invalid_element_errors
221
- [
222
- ::Selenium::WebDriver::Error::StaleElementReferenceError,
223
- ::Selenium::WebDriver::Error::UnhandledError,
224
- ::Selenium::WebDriver::Error::ElementNotVisibleError,
225
- ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a chromedriver go_back/go_forward race condition
226
- ::Selenium::WebDriver::Error::ElementNotInteractableError,
227
- ::Selenium::WebDriver::Error::ElementClickInterceptedError,
228
- ::Selenium::WebDriver::Error::InvalidElementStateError,
229
- ::Selenium::WebDriver::Error::ElementNotSelectableError,
230
- ::Selenium::WebDriver::Error::ElementNotSelectableError,
231
- ::Selenium::WebDriver::Error::NoSuchElementError, # IE
232
- ::Selenium::WebDriver::Error::InvalidArgumentError # IE
233
- ]
298
+ @invalid_element_errors ||=
299
+ [
300
+ ::Selenium::WebDriver::Error::StaleElementReferenceError,
301
+ ::Selenium::WebDriver::Error::ElementNotInteractableError,
302
+ ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around chromedriver go_back/go_forward race condition
303
+ ::Selenium::WebDriver::Error::ElementClickInterceptedError,
304
+ ::Selenium::WebDriver::Error::NoSuchElementError, # IE
305
+ ::Selenium::WebDriver::Error::InvalidArgumentError # IE
306
+ ].tap do |errors|
307
+ if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
308
+ errors.push(::Selenium::WebDriver::Error::DetachedShadowRootError)
309
+ end
310
+ end
234
311
  end
235
312
 
236
313
  def no_such_window_error
@@ -243,15 +320,27 @@ private
243
320
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
244
321
  end
245
322
 
323
+ def native_active_element
324
+ browser.switch_to.active_element
325
+ end
326
+
246
327
  def clear_browser_state
247
328
  delete_all_cookies
248
329
  clear_storage
249
- rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
330
+ rescue *clear_browser_state_errors
250
331
  # delete_all_cookies fails when we've previously gone
251
332
  # to about:blank, so we rescue this error and do nothing
252
333
  # instead.
253
334
  end
254
335
 
336
+ def clear_browser_state_errors
337
+ @clear_browser_state_errors ||= [Selenium::WebDriver::Error::UnknownError]
338
+ end
339
+
340
+ def unhandled_alert_errors
341
+ @unhandled_alert_errors ||= [Selenium::WebDriver::Error::UnexpectedAlertOpenError]
342
+ end
343
+
255
344
  def delete_all_cookies
256
345
  @browser.manage.delete_all_cookies
257
346
  end
@@ -259,7 +348,7 @@ private
259
348
  def clear_storage
260
349
  clear_session_storage unless options[:clear_session_storage] == false
261
350
  clear_local_storage unless options[:clear_local_storage] == false
262
- rescue Selenium::WebDriver::Error::JavascriptError # rubocop:disable Lint/HandleExceptions
351
+ rescue Selenium::WebDriver::Error::JavascriptError
263
352
  # session/local storage may not be available if on non-http pages (e.g. about:blank)
264
353
  end
265
354
 
@@ -267,7 +356,13 @@ private
267
356
  if @browser.respond_to? :session_storage
268
357
  @browser.session_storage.clear
269
358
  else
270
- warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
359
+ begin
360
+ @browser&.execute_script('window.sessionStorage.clear()')
361
+ rescue # rubocop:disable Style/RescueStandardError
362
+ unless options[:clear_session_storage].nil?
363
+ warn 'sessionStorage clear requested but is not supported by this driver'
364
+ end
365
+ end
271
366
  end
272
367
  end
273
368
 
@@ -275,7 +370,13 @@ private
275
370
  if @browser.respond_to? :local_storage
276
371
  @browser.local_storage.clear
277
372
  else
278
- warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
373
+ begin
374
+ @browser&.execute_script('window.localStorage.clear()')
375
+ rescue # rubocop:disable Style/RescueStandardError
376
+ unless options[:clear_local_storage].nil?
377
+ warn 'localStorage clear requested but is not supported by this driver'
378
+ end
379
+ end
279
380
  end
280
381
  end
281
382
 
@@ -283,7 +384,7 @@ private
283
384
  @browser.navigate.to(url)
284
385
  sleep 0.1 # slight wait for alert
285
386
  @browser.switch_to.alert.accept
286
- rescue modal_error # rubocop:disable Lint/HandleExceptions
387
+ rescue modal_error
287
388
  # alert now gone, should mean navigation happened
288
389
  end
289
390
 
@@ -313,16 +414,25 @@ private
313
414
  begin
314
415
  wait.until do
315
416
  alert = @browser.switch_to.alert
316
- regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
317
- alert.text.match(regexp) ? alert : nil
417
+ regexp = text.is_a?(Regexp) ? text : Regexp.new(Regexp.escape(text.to_s))
418
+ matched = alert.text.match?(regexp)
419
+ unless matched
420
+ raise Capybara::ModalNotFound, "Unable to find modal dialog with #{text} - found '#{alert.text}' instead."
421
+ end
422
+
423
+ alert
318
424
  end
319
- rescue Selenium::WebDriver::Error::TimeOutError
425
+ rescue *find_modal_errors
320
426
  raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
321
427
  end
322
428
  end
323
429
 
430
+ def find_modal_errors
431
+ @find_modal_errors ||= [Selenium::WebDriver::Error::TimeoutError]
432
+ end
433
+
324
434
  def silenced_unknown_error_message?(msg)
325
- silenced_unknown_error_messages.any? { |regex| msg =~ regex }
435
+ silenced_unknown_error_messages.any? { |regex| msg.match? regex }
326
436
  end
327
437
 
328
438
  def silenced_unknown_error_messages
@@ -334,8 +444,8 @@ private
334
444
  when Array
335
445
  arg.map { |arr| unwrap_script_result(arr) }
336
446
  when Hash
337
- arg.each { |key, value| arg[key] = unwrap_script_result(value) }
338
- when Selenium::WebDriver::Element
447
+ arg.transform_values! { |value| unwrap_script_result(value) }
448
+ when Selenium::WebDriver::Element, Selenium::WebDriver::ShadowRoot
339
449
  build_node(arg)
340
450
  else
341
451
  arg
@@ -350,20 +460,15 @@ private
350
460
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
351
461
  end
352
462
 
353
- def specialize_driver(sel_driver)
354
- case sel_driver.browser
355
- when :chrome
356
- extend ChromeDriver
357
- when :firefox
358
- require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(sel_driver)
359
- extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
360
- when :ie, :internet_explorer
361
- extend InternetExplorerDriver
362
- end
463
+ def bridge
464
+ browser.send(:bridge)
363
465
  end
364
466
 
365
- def pause_broken?(driver)
366
- driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
467
+ def specialize_driver
468
+ browser_type = browser.browser
469
+ Capybara::Selenium::Driver.specializations.select { |k, _v| k === browser_type }.each_value do |specialization| # rubocop:disable Style/CaseEquality
470
+ extend specialization
471
+ end
367
472
  end
368
473
 
369
474
  def setup_exit_handler
@@ -386,6 +491,11 @@ private
386
491
  raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?
387
492
 
388
493
  sleep 0.01
494
+
495
+ # It has been observed that it is possible that asynchronous JS code in
496
+ # the application under test can navigate the browser away from about:blank
497
+ # if the timing is just right. Ensure we are still at about:blank...
498
+ @browser.navigate.to('about:blank') unless current_url == 'about:blank'
389
499
  end
390
500
  end
391
501
 
@@ -403,3 +513,5 @@ end
403
513
  require 'capybara/selenium/driver_specializations/chrome_driver'
404
514
  require 'capybara/selenium/driver_specializations/firefox_driver'
405
515
  require 'capybara/selenium/driver_specializations/internet_explorer_driver'
516
+ require 'capybara/selenium/driver_specializations/safari_driver'
517
+ require 'capybara/selenium/driver_specializations/edge_driver'
@@ -1,25 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/selenium/nodes/chrome_node'
4
+ require 'capybara/selenium/patches/logs'
4
5
 
5
6
  module Capybara::Selenium::Driver::ChromeDriver
7
+ def self.extended(base)
8
+ bridge = base.send(:bridge)
9
+ bridge.extend Capybara::Selenium::ChromeLogs unless bridge.respond_to?(:log)
10
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
11
+ base.options[:native_displayed] = false if base.options[:native_displayed].nil?
12
+ end
13
+
6
14
  def fullscreen_window(handle)
7
15
  within_given_window(handle) do
8
- begin
9
- super
10
- rescue NoMethodError => err
11
- raise unless err.message =~ /full_screen_window/
12
-
13
- result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
14
- result['value']
15
- end
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.include?('full_screen_window')
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
16
22
  end
17
23
  end
18
24
 
19
25
  def resize_window_to(handle, width, height)
20
26
  super
21
- rescue Selenium::WebDriver::Error::UnknownError => err
22
- raise unless err.message =~ /failed to change window state/
27
+ rescue Selenium::WebDriver::Error::UnknownError => e
28
+ raise unless e.message.include?('failed to change window state')
23
29
 
24
30
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
25
31
  # and raises unnecessary error. Wait a bit and try again.
@@ -32,30 +38,80 @@ module Capybara::Selenium::Driver::ChromeDriver
32
38
  return unless @browser
33
39
 
34
40
  switch_to_window(window_handles.first)
35
- window_handles.slice(1..-1).each { |win| close_window(win) }
36
- super
41
+ window_handles.slice(1..).each { |win| close_window(win) }
42
+ return super if chromedriver_version < 73
43
+
44
+ timer = Capybara::Helpers.timer(expire_in: 10)
45
+ begin
46
+ clear_storage unless uniform_storage_clear?
47
+ @browser.navigate.to('about:blank')
48
+ wait_for_empty_page(timer)
49
+ rescue *unhandled_alert_errors
50
+ accept_unhandled_reset_alert
51
+ retry
52
+ end
53
+
54
+ execute_cdp('Storage.clearDataForOrigin', origin: '*', storageTypes: storage_types_to_clear)
37
55
  end
38
56
 
39
57
  private
40
58
 
59
+ def storage_types_to_clear
60
+ types = ['cookies']
61
+ types << 'local_storage' if clear_all_storage?
62
+ types.join(',')
63
+ end
64
+
65
+ def clear_all_storage?
66
+ storage_clears.none? false
67
+ end
68
+
69
+ def uniform_storage_clear?
70
+ storage_clears.uniq { |s| s == false }.length <= 1
71
+ end
72
+
73
+ def storage_clears
74
+ options.values_at(:clear_session_storage, :clear_local_storage)
75
+ end
76
+
77
+ def clear_storage
78
+ # Chrome errors if attempt to clear storage on about:blank
79
+ # In W3C mode it crashes chromedriver
80
+ url = current_url
81
+ super unless url.nil? || url.start_with?('about:')
82
+ end
83
+
41
84
  def delete_all_cookies
42
85
  execute_cdp('Network.clearBrowserCookies')
43
- rescue Selenium::WebDriver::Error::UnhandledError, Selenium::WebDriver::Error::WebDriverError
86
+ rescue *cdp_unsupported_errors
44
87
  # If the CDP clear isn't supported do original limited clear
45
88
  super
46
89
  end
47
90
 
91
+ def cdp_unsupported_errors
92
+ @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError]
93
+ end
94
+
48
95
  def execute_cdp(cmd, params = {})
49
- args = { cmd: cmd, params: params }
50
- result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
51
- result['value']
96
+ if browser.respond_to? :execute_cdp
97
+ browser.execute_cdp(cmd, **params)
98
+ else
99
+ args = { cmd: cmd, params: params }
100
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
101
+ result['value']
102
+ end
52
103
  end
53
104
 
54
105
  def build_node(native_node, initial_cache = {})
55
106
  ::Capybara::Selenium::ChromeNode.new(self, native_node, initial_cache)
56
107
  end
57
108
 
58
- def bridge
59
- browser.send(:bridge)
109
+ def chromedriver_version
110
+ @chromedriver_version ||= begin
111
+ caps = browser.capabilities
112
+ caps['chrome']&.fetch('chromedriverVersion', nil).to_f
113
+ end
60
114
  end
61
115
  end
116
+
117
+ Capybara::Selenium::Driver.register_specialization :chrome, Capybara::Selenium::Driver::ChromeDriver
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selenium/nodes/edge_node'
4
+
5
+ module Capybara::Selenium::Driver::EdgeDriver
6
+ def self.extended(base)
7
+ bridge = base.send(:bridge)
8
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
9
+ base.options[:native_displayed] = false if base.options[:native_displayed].nil?
10
+ end
11
+
12
+ def fullscreen_window(handle)
13
+ return super if edgedriver_version < 75
14
+
15
+ within_given_window(handle) do
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.include?('full_screen_window')
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
22
+ end
23
+ end
24
+
25
+ def resize_window_to(handle, width, height)
26
+ super
27
+ rescue Selenium::WebDriver::Error::UnknownError => e
28
+ raise unless e.message.include?('failed to change window state')
29
+
30
+ # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
31
+ # and raises unnecessary error. Wait a bit and try again.
32
+ sleep 0.25
33
+ super
34
+ end
35
+
36
+ def reset!
37
+ return super if edgedriver_version < 75
38
+ # Use instance variable directly so we avoid starting the browser just to reset the session
39
+ return unless @browser
40
+
41
+ switch_to_window(window_handles.first)
42
+ window_handles.slice(1..).each { |win| close_window(win) }
43
+
44
+ timer = Capybara::Helpers.timer(expire_in: 10)
45
+ begin
46
+ clear_storage unless uniform_storage_clear?
47
+ @browser.navigate.to('about:blank')
48
+ wait_for_empty_page(timer)
49
+ rescue *unhandled_alert_errors
50
+ accept_unhandled_reset_alert
51
+ retry
52
+ end
53
+
54
+ execute_cdp('Storage.clearDataForOrigin', origin: '*', storageTypes: storage_types_to_clear)
55
+ end
56
+
57
+ def download_path=(path)
58
+ if @browser.respond_to?(:download_path=)
59
+ @browser.download_path = path
60
+ else
61
+ # Not yet implemented in seleniun-webdriver for edge so do it ourselves
62
+ execute_cdp('Page.setDownloadBehavior', behavior: 'allow', downloadPath: path)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def storage_types_to_clear
69
+ types = ['cookies']
70
+ types << 'local_storage' if clear_all_storage?
71
+ types.join(',')
72
+ end
73
+
74
+ def clear_all_storage?
75
+ storage_clears.none? false
76
+ end
77
+
78
+ def uniform_storage_clear?
79
+ storage_clears.uniq { |s| s == false }.length <= 1
80
+ end
81
+
82
+ def storage_clears
83
+ options.values_at(:clear_session_storage, :clear_local_storage)
84
+ end
85
+
86
+ def clear_storage
87
+ # Edgedriver crashes if attempt to clear storage on about:blank
88
+ url = current_url
89
+ super unless url.nil? || url.start_with?('about:')
90
+ end
91
+
92
+ def delete_all_cookies
93
+ return super if edgedriver_version < 75
94
+
95
+ execute_cdp('Network.clearBrowserCookies')
96
+ rescue *cdp_unsupported_errors
97
+ # If the CDP clear isn't supported do original limited clear
98
+ super
99
+ end
100
+
101
+ def cdp_unsupported_errors
102
+ @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError]
103
+ end
104
+
105
+ def execute_cdp(cmd, params = {})
106
+ if browser.respond_to? :execute_cdp
107
+ browser.execute_cdp(cmd, **params)
108
+ else
109
+ args = { cmd: cmd, params: params }
110
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/ms/cdp/execute", args)
111
+ result['value']
112
+ end
113
+ end
114
+
115
+ def build_node(native_node, initial_cache = {})
116
+ ::Capybara::Selenium::EdgeNode.new(self, native_node, initial_cache)
117
+ end
118
+
119
+ def edgedriver_version
120
+ @edgedriver_version ||= begin
121
+ caps = browser.capabilities
122
+ caps['msedge']&.fetch('msedgedriverVersion', nil).to_f
123
+ end
124
+ end
125
+ end
126
+
127
+ Capybara::Selenium::Driver.register_specialization :edge, Capybara::Selenium::Driver::EdgeDriver
128
+ Capybara::Selenium::Driver.register_specialization :edge_chrome, Capybara::Selenium::Driver::EdgeDriver