capybara 3.8.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +465 -0
  4. data/License.txt +1 -1
  5. data/README.md +58 -57
  6. data/lib/capybara/config.rb +10 -4
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +2 -2
  9. data/lib/capybara/driver/node.rb +26 -5
  10. data/lib/capybara/dsl.rb +12 -4
  11. data/lib/capybara/helpers.rb +8 -4
  12. data/lib/capybara/minitest/spec.rb +162 -85
  13. data/lib/capybara/minitest.rb +248 -148
  14. data/lib/capybara/node/actions.rb +149 -96
  15. data/lib/capybara/node/base.rb +27 -10
  16. data/lib/capybara/node/document.rb +12 -0
  17. data/lib/capybara/node/document_matchers.rb +9 -5
  18. data/lib/capybara/node/element.rb +254 -109
  19. data/lib/capybara/node/finders.rb +83 -76
  20. data/lib/capybara/node/matchers.rb +279 -141
  21. data/lib/capybara/node/simple.rb +25 -6
  22. data/lib/capybara/queries/ancestor_query.rb +5 -7
  23. data/lib/capybara/queries/base_query.rb +11 -5
  24. data/lib/capybara/queries/current_path_query.rb +3 -3
  25. data/lib/capybara/queries/match_query.rb +1 -0
  26. data/lib/capybara/queries/selector_query.rb +467 -103
  27. data/lib/capybara/queries/sibling_query.rb +5 -4
  28. data/lib/capybara/queries/style_query.rb +6 -2
  29. data/lib/capybara/queries/text_query.rb +17 -3
  30. data/lib/capybara/queries/title_query.rb +2 -2
  31. data/lib/capybara/rack_test/browser.rb +22 -15
  32. data/lib/capybara/rack_test/driver.rb +10 -1
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +33 -28
  35. data/lib/capybara/rack_test/node.rb +74 -6
  36. data/lib/capybara/registration_container.rb +44 -0
  37. data/lib/capybara/registrations/drivers.rb +36 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  39. data/lib/capybara/registrations/servers.rb +44 -0
  40. data/lib/capybara/result.rb +55 -23
  41. data/lib/capybara/rspec/features.rb +4 -4
  42. data/lib/capybara/rspec/matcher_proxies.rb +36 -15
  43. data/lib/capybara/rspec/matchers/base.rb +111 -0
  44. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  45. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  46. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  47. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  48. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  49. data/lib/capybara/rspec/matchers/have_selector.rb +77 -0
  50. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  51. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  52. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  53. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  54. data/lib/capybara/rspec/matchers/match_style.rb +38 -0
  55. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  56. data/lib/capybara/rspec/matchers.rb +117 -311
  57. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  58. data/lib/capybara/selector/builders/xpath_builder.rb +69 -0
  59. data/lib/capybara/selector/css.rb +17 -15
  60. data/lib/capybara/selector/definition/button.rb +52 -0
  61. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  62. data/lib/capybara/selector/definition/css.rb +10 -0
  63. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  64. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  65. data/lib/capybara/selector/definition/element.rb +27 -0
  66. data/lib/capybara/selector/definition/field.rb +40 -0
  67. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  68. data/lib/capybara/selector/definition/file_field.rb +13 -0
  69. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  70. data/lib/capybara/selector/definition/frame.rb +17 -0
  71. data/lib/capybara/selector/definition/id.rb +6 -0
  72. data/lib/capybara/selector/definition/label.rb +62 -0
  73. data/lib/capybara/selector/definition/link.rb +54 -0
  74. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  75. data/lib/capybara/selector/definition/option.rb +27 -0
  76. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  77. data/lib/capybara/selector/definition/select.rb +81 -0
  78. data/lib/capybara/selector/definition/table.rb +109 -0
  79. data/lib/capybara/selector/definition/table_row.rb +21 -0
  80. data/lib/capybara/selector/definition/xpath.rb +5 -0
  81. data/lib/capybara/selector/definition.rb +277 -0
  82. data/lib/capybara/selector/filter.rb +1 -0
  83. data/lib/capybara/selector/filter_set.rb +26 -19
  84. data/lib/capybara/selector/filters/base.rb +24 -5
  85. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  86. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  87. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  88. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  89. data/lib/capybara/selector/selector.rb +73 -367
  90. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  91. data/lib/capybara/selector.rb +221 -480
  92. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  93. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  94. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  95. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  96. data/lib/capybara/selenium/driver.rb +203 -86
  97. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +88 -14
  98. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  99. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -0
  100. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  101. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  102. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  103. data/lib/capybara/selenium/extensions/find.rb +110 -0
  104. data/lib/capybara/selenium/extensions/html5_drag.rb +191 -22
  105. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  106. data/lib/capybara/selenium/extensions/scroll.rb +78 -0
  107. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  108. data/lib/capybara/selenium/node.rb +298 -93
  109. data/lib/capybara/selenium/nodes/chrome_node.rb +100 -8
  110. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  111. data/lib/capybara/selenium/nodes/firefox_node.rb +131 -0
  112. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  113. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  114. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  115. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  116. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  117. data/lib/capybara/selenium/patches/logs.rb +45 -0
  118. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -3
  119. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  120. data/lib/capybara/server/animation_disabler.rb +4 -3
  121. data/lib/capybara/server/checker.rb +6 -2
  122. data/lib/capybara/server/middleware.rb +23 -13
  123. data/lib/capybara/server.rb +30 -7
  124. data/lib/capybara/session/config.rb +14 -10
  125. data/lib/capybara/session/matchers.rb +11 -7
  126. data/lib/capybara/session.rb +152 -111
  127. data/lib/capybara/spec/public/offset.js +6 -0
  128. data/lib/capybara/spec/public/test.js +101 -10
  129. data/lib/capybara/spec/session/all_spec.rb +96 -6
  130. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  131. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +29 -0
  132. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  133. data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
  134. data/lib/capybara/spec/session/assert_style_spec.rb +4 -4
  135. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  136. data/lib/capybara/spec/session/attach_file_spec.rb +63 -36
  137. data/lib/capybara/spec/session/check_spec.rb +10 -4
  138. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  139. data/lib/capybara/spec/session/click_button_spec.rb +117 -61
  140. data/lib/capybara/spec/session/click_link_or_button_spec.rb +16 -0
  141. data/lib/capybara/spec/session/click_link_spec.rb +17 -6
  142. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  143. data/lib/capybara/spec/session/evaluate_script_spec.rb +13 -0
  144. data/lib/capybara/spec/session/execute_script_spec.rb +1 -0
  145. data/lib/capybara/spec/session/fill_in_spec.rb +47 -6
  146. data/lib/capybara/spec/session/find_field_spec.rb +1 -1
  147. data/lib/capybara/spec/session/find_spec.rb +74 -4
  148. data/lib/capybara/spec/session/first_spec.rb +1 -1
  149. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +13 -1
  150. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  151. data/lib/capybara/spec/session/has_all_selectors_spec.rb +1 -1
  152. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  153. data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
  154. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  155. data/lib/capybara/spec/session/has_css_spec.rb +122 -12
  156. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  157. data/lib/capybara/spec/session/has_field_spec.rb +55 -0
  158. data/lib/capybara/spec/session/has_select_spec.rb +34 -6
  159. data/lib/capybara/spec/session/has_selector_spec.rb +11 -4
  160. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  161. data/lib/capybara/spec/session/has_table_spec.rb +166 -0
  162. data/lib/capybara/spec/session/has_text_spec.rb +48 -1
  163. data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
  164. data/lib/capybara/spec/session/html_spec.rb +7 -0
  165. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  166. data/lib/capybara/spec/session/node_spec.rb +643 -18
  167. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  168. data/lib/capybara/spec/session/refresh_spec.rb +4 -0
  169. data/lib/capybara/spec/session/reset_session_spec.rb +23 -8
  170. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  171. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  172. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  173. data/lib/capybara/spec/session/select_spec.rb +10 -10
  174. data/lib/capybara/spec/session/selectors_spec.rb +36 -5
  175. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  176. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  178. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +4 -0
  179. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -0
  180. data/lib/capybara/spec/session/window/window_spec.rb +59 -58
  181. data/lib/capybara/spec/session/window/windows_spec.rb +4 -0
  182. data/lib/capybara/spec/session/within_spec.rb +23 -0
  183. data/lib/capybara/spec/spec_helper.rb +16 -6
  184. data/lib/capybara/spec/test_app.rb +28 -23
  185. data/lib/capybara/spec/views/animated.erb +49 -0
  186. data/lib/capybara/spec/views/form.erb +48 -7
  187. data/lib/capybara/spec/views/frame_child.erb +3 -2
  188. data/lib/capybara/spec/views/frame_one.erb +1 -0
  189. data/lib/capybara/spec/views/obscured.erb +47 -0
  190. data/lib/capybara/spec/views/offset.erb +32 -0
  191. data/lib/capybara/spec/views/react.erb +45 -0
  192. data/lib/capybara/spec/views/scroll.erb +20 -0
  193. data/lib/capybara/spec/views/spatial.erb +31 -0
  194. data/lib/capybara/spec/views/tables.erb +67 -0
  195. data/lib/capybara/spec/views/with_animation.erb +29 -1
  196. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  197. data/lib/capybara/spec/views/with_hover.erb +1 -0
  198. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  199. data/lib/capybara/spec/views/with_html.erb +32 -6
  200. data/lib/capybara/spec/views/with_js.erb +3 -1
  201. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  202. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  203. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  204. data/lib/capybara/version.rb +1 -1
  205. data/lib/capybara/window.rb +11 -11
  206. data/lib/capybara.rb +118 -111
  207. data/spec/basic_node_spec.rb +14 -3
  208. data/spec/capybara_spec.rb +29 -29
  209. data/spec/css_builder_spec.rb +101 -0
  210. data/spec/dsl_spec.rb +46 -21
  211. data/spec/filter_set_spec.rb +5 -5
  212. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  213. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  214. data/spec/minitest_spec.rb +18 -4
  215. data/spec/minitest_spec_spec.rb +59 -44
  216. data/spec/rack_test_spec.rb +117 -89
  217. data/spec/regexp_dissassembler_spec.rb +250 -0
  218. data/spec/result_spec.rb +51 -49
  219. data/spec/rspec/features_spec.rb +3 -0
  220. data/spec/rspec/shared_spec_matchers.rb +112 -97
  221. data/spec/rspec_spec.rb +35 -17
  222. data/spec/sauce_spec_chrome.rb +43 -0
  223. data/spec/selector_spec.rb +244 -28
  224. data/spec/selenium_spec_chrome.rb +125 -54
  225. data/spec/selenium_spec_chrome_remote.rb +26 -12
  226. data/spec/selenium_spec_edge.rb +23 -8
  227. data/spec/selenium_spec_firefox.rb +208 -0
  228. data/spec/selenium_spec_firefox_remote.rb +15 -18
  229. data/spec/selenium_spec_ie.rb +82 -13
  230. data/spec/selenium_spec_safari.rb +148 -0
  231. data/spec/server_spec.rb +118 -77
  232. data/spec/session_spec.rb +19 -3
  233. data/spec/shared_selenium_node.rb +83 -0
  234. data/spec/shared_selenium_session.rb +110 -65
  235. data/spec/spec_helper.rb +57 -9
  236. data/spec/xpath_builder_spec.rb +93 -0
  237. metadata +257 -17
  238. data/lib/capybara/rspec/compound.rb +0 -94
  239. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +0 -49
  240. data/lib/capybara/selenium/nodes/marionette_node.rb +0 -121
  241. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
  242. data/spec/selenium_spec_marionette.rb +0 -172
@@ -4,29 +4,55 @@ require 'uri'
4
4
  require 'English'
5
5
 
6
6
  class Capybara::Selenium::Driver < Capybara::Driver::Base
7
+ include Capybara::Selenium::Find
8
+
7
9
  DEFAULT_OPTIONS = {
8
10
  browser: :firefox,
9
- clear_local_storage: false,
10
- clear_session_storage: false
11
+ clear_local_storage: nil,
12
+ clear_session_storage: nil
11
13
  }.freeze
12
- SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage].freeze
14
+ SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout native_displayed].freeze
13
15
  attr_reader :app, :options
14
16
 
15
- def self.load_selenium
16
- require 'selenium-webdriver'
17
- 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')
18
- rescue LoadError => err
19
- raise err if err.message !~ /selenium-webdriver/
20
- 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."
17
+ class << self
18
+ def load_selenium
19
+ require 'selenium-webdriver'
20
+ require 'capybara/selenium/logger_suppressor'
21
+ require 'capybara/selenium/patches/atoms'
22
+ require 'capybara/selenium/patches/is_displayed'
23
+ require 'capybara/selenium/patches/action_pauser'
24
+ if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
25
+ warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
26
+ end
27
+ rescue LoadError => e
28
+ raise e unless e.message.match?(/selenium-webdriver/)
29
+
30
+ 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."
31
+ end
32
+
33
+ attr_reader :specializations
34
+
35
+ def register_specialization(browser_name, specialization)
36
+ @specializations ||= {}
37
+ @specializations[browser_name] = specialization
38
+ end
21
39
  end
22
40
 
23
41
  def browser
24
- @browser ||= begin
25
- processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
26
- Selenium::WebDriver.for(options[:browser], processed_options).tap do |driver|
27
- specialize_driver(driver)
28
- setup_exit_handler
42
+ unless @browser
43
+ options[:http_client] ||= begin
44
+ require 'capybara/selenium/patches/persistent_client'
45
+ if options[:timeout]
46
+ ::Capybara::Selenium::PersistentClient.new(read_timeout: options[:timeout])
47
+ else
48
+ ::Capybara::Selenium::PersistentClient.new
49
+ end
29
50
  end
51
+ processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
52
+ @browser = Selenium::WebDriver.for(options[:browser], processed_options)
53
+
54
+ specialize_driver
55
+ setup_exit_handler
30
56
  end
31
57
  @browser
32
58
  end
@@ -59,6 +85,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
59
85
 
60
86
  def html
61
87
  browser.page_source
88
+ rescue Selenium::WebDriver::Error::JavascriptError => e
89
+ raise unless e.message.match?(/documentElement is null/)
62
90
  end
63
91
 
64
92
  def title
@@ -69,14 +97,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
69
97
  browser.current_url
70
98
  end
71
99
 
72
- def find_xpath(selector)
73
- browser.find_elements(:xpath, selector).map(&method(:build_node))
74
- end
75
-
76
- def find_css(selector)
77
- browser.find_elements(:css, selector).map(&method(:build_node))
78
- end
79
-
80
100
  def wait?; true; end
81
101
  def needs_server?; true; end
82
102
 
@@ -106,37 +126,34 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
106
126
  navigated = false
107
127
  timer = Capybara::Helpers.timer(expire_in: 10)
108
128
  begin
109
- unless navigated
110
- # Only trigger a navigation if we haven't done it already, otherwise it
111
- # can trigger an endless series of unload modals
112
- clear_browser_state
113
- @browser.navigate.to('about:blank')
114
- end
129
+ # Only trigger a navigation if we haven't done it already, otherwise it
130
+ # can trigger an endless series of unload modals
131
+ reset_browser_state unless navigated
115
132
  navigated = true
116
-
117
133
  # Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
118
- until find_xpath('/html/body/*').empty?
119
- raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?
120
- sleep 0.05
121
- end
122
- rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
134
+ wait_for_empty_page(timer)
135
+ rescue *unhandled_alert_errors
123
136
  # This error is thrown if an unhandled alert is on the page
124
137
  # Firefox appears to automatically dismiss this alert, chrome does not
125
138
  # We'll try to accept it
126
- begin
127
- @browser.switch_to.alert.accept
128
- sleep 0.25 # allow time for the modal to be handled
129
- rescue modal_error
130
- # The alert is now gone.
131
- # If navigation has not occurred attempt again and accept alert
132
- # since FF may have dismissed the alert at first attempt.
133
- navigate_with_accept('about:blank') if current_url != 'about:blank'
134
- end
139
+ accept_unhandled_reset_alert
135
140
  # try cleaning up the browser again
136
141
  retry
137
142
  end
138
143
  end
139
144
 
145
+ def frame_obscured_at?(x:, y:)
146
+ frame = @frame_handles[current_window_handle].last
147
+ return false unless frame
148
+
149
+ switch_to_frame(:parent)
150
+ begin
151
+ frame.base.obscured?(x: x, y: y)
152
+ ensure
153
+ switch_to_frame(frame)
154
+ end
155
+ end
156
+
140
157
  def switch_to_frame(frame)
141
158
  handles = @frame_handles[current_window_handle]
142
159
  case frame
@@ -147,7 +164,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
147
164
  handles.pop
148
165
  browser.switch_to.parent_frame
149
166
  else
150
- handles << frame.native
167
+ handles << frame
151
168
  browser.switch_to.frame(frame.native)
152
169
  end
153
170
  end
@@ -184,6 +201,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
184
201
 
185
202
  def close_window(handle)
186
203
  raise ArgumentError, 'Not allowed to close the primary window' if handle == window_handles.first
204
+
187
205
  within_given_window(handle) do
188
206
  browser.close
189
207
  end
@@ -193,7 +211,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
193
211
  browser.window_handles
194
212
  end
195
213
 
196
- def open_new_window
214
+ def open_new_window(kind = :tab)
215
+ browser.manage.new_window(kind)
216
+ rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
217
+ # If not supported by the driver or browser default to using JS
197
218
  browser.execute_script('window.open();')
198
219
  end
199
220
 
@@ -203,7 +224,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
203
224
 
204
225
  def accept_modal(_type, **options)
205
226
  yield if block_given?
206
- modal = find_modal(options)
227
+ modal = find_modal(**options)
207
228
 
208
229
  modal.send_keys options[:with] if options[:with]
209
230
 
@@ -214,7 +235,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
214
235
 
215
236
  def dismiss_modal(_type, **options)
216
237
  yield if block_given?
217
- modal = find_modal(options)
238
+ modal = find_modal(**options)
218
239
  message = modal.text
219
240
  modal.dismiss
220
241
  message
@@ -222,31 +243,39 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
222
243
 
223
244
  def quit
224
245
  @browser&.quit
225
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/HandleExceptions
246
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
226
247
  # Browser must have already gone
227
- rescue Selenium::WebDriver::Error::UnknownError => err
228
- unless silenced_unknown_error_message?(err.message) # Most likely already gone
248
+ rescue Selenium::WebDriver::Error::UnknownError => e
249
+ unless silenced_unknown_error_message?(e.message) # Most likely already gone
229
250
  # probably already gone but not sure - so warn
230
- warn "Ignoring Selenium UnknownError during driver quit: #{err.message}"
251
+ warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
231
252
  end
232
253
  ensure
233
254
  @browser = nil
234
255
  end
235
256
 
236
257
  def invalid_element_errors
237
- [
238
- ::Selenium::WebDriver::Error::StaleElementReferenceError,
239
- ::Selenium::WebDriver::Error::UnhandledError,
240
- ::Selenium::WebDriver::Error::ElementNotVisibleError,
241
- ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
242
- ::Selenium::WebDriver::Error::ElementNotInteractableError,
243
- ::Selenium::WebDriver::Error::ElementClickInterceptedError,
244
- ::Selenium::WebDriver::Error::InvalidElementStateError,
245
- ::Selenium::WebDriver::Error::ElementNotSelectableError,
246
- ::Selenium::WebDriver::Error::ElementNotSelectableError,
247
- ::Selenium::WebDriver::Error::NoSuchElementError, # IE
248
- ::Selenium::WebDriver::Error::InvalidArgumentError # IE
249
- ]
258
+ @invalid_element_errors ||= begin
259
+ [
260
+ ::Selenium::WebDriver::Error::StaleElementReferenceError,
261
+ ::Selenium::WebDriver::Error::ElementNotInteractableError,
262
+ ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around chromedriver go_back/go_forward race condition
263
+ ::Selenium::WebDriver::Error::ElementClickInterceptedError,
264
+ ::Selenium::WebDriver::Error::NoSuchElementError, # IE
265
+ ::Selenium::WebDriver::Error::InvalidArgumentError # IE
266
+ ].tap do |errors|
267
+ unless selenium_4?
268
+ ::Selenium::WebDriver.logger.suppress_deprecations do
269
+ errors.concat [
270
+ ::Selenium::WebDriver::Error::UnhandledError,
271
+ ::Selenium::WebDriver::Error::ElementNotVisibleError,
272
+ ::Selenium::WebDriver::Error::InvalidElementStateError,
273
+ ::Selenium::WebDriver::Error::ElementNotSelectableError
274
+ ]
275
+ end
276
+ end
277
+ end
278
+ end
250
279
  end
251
280
 
252
281
  def no_such_window_error
@@ -255,29 +284,56 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
255
284
 
256
285
  private
257
286
 
287
+ def selenium_4?
288
+ defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)
289
+ end
290
+
258
291
  def native_args(args)
259
292
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
260
293
  end
261
294
 
262
295
  def clear_browser_state
263
- @browser.manage.delete_all_cookies
296
+ delete_all_cookies
264
297
  clear_storage
265
- rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
298
+ rescue *clear_browser_state_errors
266
299
  # delete_all_cookies fails when we've previously gone
267
300
  # to about:blank, so we rescue this error and do nothing
268
301
  # instead.
269
302
  end
270
303
 
304
+ def clear_browser_state_errors
305
+ @clear_browser_state_errors ||= [Selenium::WebDriver::Error::UnknownError]
306
+ end
307
+
308
+ def unhandled_alert_errors
309
+ @unhandled_alert_errors ||= with_legacy_error(
310
+ [Selenium::WebDriver::Error::UnexpectedAlertOpenError],
311
+ 'UnhandledAlertError'
312
+ )
313
+ end
314
+
315
+ def delete_all_cookies
316
+ @browser.manage.delete_all_cookies
317
+ end
318
+
271
319
  def clear_storage
272
- clear_session_storage if options[:clear_session_storage]
273
- clear_local_storage if options[:clear_local_storage]
320
+ clear_session_storage unless options[:clear_session_storage] == false
321
+ clear_local_storage unless options[:clear_local_storage] == false
322
+ rescue Selenium::WebDriver::Error::JavascriptError
323
+ # session/local storage may not be available if on non-http pages (e.g. about:blank)
274
324
  end
275
325
 
276
326
  def clear_session_storage
277
327
  if @browser.respond_to? :session_storage
278
328
  @browser.session_storage.clear
279
329
  else
280
- warn 'sessionStorage clear requested but is not available for this driver'
330
+ begin
331
+ @browser&.execute_script('window.sessionStorage.clear()')
332
+ rescue # rubocop:disable Style/RescueStandardError
333
+ unless options[:clear_session_storage].nil?
334
+ warn 'sessionStorage clear requested but is not supported by this driver'
335
+ end
336
+ end
281
337
  end
282
338
  end
283
339
 
@@ -285,7 +341,13 @@ private
285
341
  if @browser.respond_to? :local_storage
286
342
  @browser.local_storage.clear
287
343
  else
288
- warn 'localStorage clear requested but is not available for this driver'
344
+ begin
345
+ @browser&.execute_script('window.localStorage.clear()')
346
+ rescue # rubocop:disable Style/RescueStandardError
347
+ unless options[:clear_local_storage].nil?
348
+ warn 'localStorage clear requested but is not supported by this driver'
349
+ end
350
+ end
289
351
  end
290
352
  end
291
353
 
@@ -293,7 +355,7 @@ private
293
355
  @browser.navigate.to(url)
294
356
  sleep 0.1 # slight wait for alert
295
357
  @browser.switch_to.alert.accept
296
- rescue modal_error # rubocop:disable Lint/HandleExceptions
358
+ rescue modal_error
297
359
  # alert now gone, should mean navigation happened
298
360
  end
299
361
 
@@ -323,16 +385,35 @@ private
323
385
  begin
324
386
  wait.until do
325
387
  alert = @browser.switch_to.alert
326
- regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
327
- alert.text.match(regexp) ? alert : nil
388
+ regexp = text.is_a?(Regexp) ? text : Regexp.new(Regexp.escape(text.to_s))
389
+ matched = alert.text.match?(regexp)
390
+ unless matched
391
+ raise Capybara::ModalNotFound, "Unable to find modal dialog with #{text} - found '#{alert.text}' instead."
392
+ end
393
+
394
+ alert
328
395
  end
329
- rescue Selenium::WebDriver::Error::TimeOutError
396
+ rescue *find_modal_errors
330
397
  raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
331
398
  end
332
399
  end
333
400
 
401
+ def find_modal_errors
402
+ @find_modal_errors ||= with_legacy_error([Selenium::WebDriver::Error::TimeoutError], 'TimeOutError')
403
+ end
404
+
405
+ def with_legacy_error(errors, legacy_error)
406
+ errors.tap do |errs|
407
+ unless selenium_4?
408
+ ::Selenium::WebDriver.logger.suppress_deprecations do
409
+ errs << Selenium::WebDriver::Error.const_get(legacy_error)
410
+ end
411
+ end
412
+ end
413
+ end
414
+
334
415
  def silenced_unknown_error_message?(msg)
335
- silenced_unknown_error_messages.any? { |regex| msg =~ regex }
416
+ silenced_unknown_error_messages.any? { |regex| msg.match? regex }
336
417
  end
337
418
 
338
419
  def silenced_unknown_error_messages
@@ -344,7 +425,7 @@ private
344
425
  when Array
345
426
  arg.map { |arr| unwrap_script_result(arr) }
346
427
  when Hash
347
- arg.each { |key, value| arg[key] = unwrap_script_result(value) }
428
+ arg.transform_values! { |value| unwrap_script_result(value) }
348
429
  when Selenium::WebDriver::Element
349
430
  build_node(arg)
350
431
  else
@@ -352,17 +433,22 @@ private
352
433
  end
353
434
  end
354
435
 
355
- def build_node(native_node)
356
- ::Capybara::Selenium::Node.new(self, native_node)
436
+ def find_context
437
+ browser
438
+ end
439
+
440
+ def build_node(native_node, initial_cache = {})
441
+ ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
442
+ end
443
+
444
+ def bridge
445
+ browser.send(:bridge)
357
446
  end
358
447
 
359
- def specialize_driver(sel_driver)
360
- case sel_driver.browser
361
- when :chrome
362
- extend ChromeDriver
363
- when :firefox
364
- require 'capybara/selenium/patches/pause_duration_fix' if sel_driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
365
- extend MarionetteDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
448
+ def specialize_driver
449
+ browser_type = browser.browser
450
+ Capybara::Selenium::Driver.specializations.select { |k, _v| k === browser_type }.each_value do |specialization| # rubocop:disable Style/CaseEquality
451
+ extend specialization
366
452
  end
367
453
  end
368
454
 
@@ -375,7 +461,38 @@ private
375
461
  exit @exit_status if @exit_status # Force exit with stored status
376
462
  end
377
463
  end
464
+
465
+ def reset_browser_state
466
+ clear_browser_state
467
+ @browser.navigate.to('about:blank')
468
+ end
469
+
470
+ def wait_for_empty_page(timer)
471
+ until find_xpath('/html/body/*').empty?
472
+ raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?
473
+
474
+ sleep 0.01
475
+
476
+ # It has been observed that it is possible that asynchronous JS code in
477
+ # the application under test can navigate the browser away from about:blank
478
+ # if the timing is just right. Ensure we are still at about:blank...
479
+ @browser.navigate.to('about:blank') unless current_url == 'about:blank'
480
+ end
481
+ end
482
+
483
+ def accept_unhandled_reset_alert
484
+ @browser.switch_to.alert.accept
485
+ sleep 0.25 # allow time for the modal to be handled
486
+ rescue modal_error
487
+ # The alert is now gone.
488
+ # If navigation has not occurred attempt again and accept alert
489
+ # since FF may have dismissed the alert at first attempt.
490
+ navigate_with_accept('about:blank') if current_url != 'about:blank'
491
+ end
378
492
  end
379
493
 
380
494
  require 'capybara/selenium/driver_specializations/chrome_driver'
381
- require 'capybara/selenium/driver_specializations/marionette_driver'
495
+ require 'capybara/selenium/driver_specializations/firefox_driver'
496
+ require 'capybara/selenium/driver_specializations/internet_explorer_driver'
497
+ require 'capybara/selenium/driver_specializations/safari_driver'
498
+ require 'capybara/selenium/driver_specializations/edge_driver'
@@ -1,28 +1,35 @@
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.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
- bridge = browser.send(:bridge)
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.match?(/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.match?(/failed to change window state/)
29
+
23
30
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
24
31
  # and raises unnecessary error. Wait a bit and try again.
25
- sleep 0.5
32
+ sleep 0.25
26
33
  super
27
34
  end
28
35
 
@@ -32,12 +39,79 @@ module Capybara::Selenium::Driver::ChromeDriver
32
39
 
33
40
  switch_to_window(window_handles.first)
34
41
  window_handles.slice(1..-1).each { |win| close_window(win) }
35
- super
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)
36
55
  end
37
56
 
38
57
  private
39
58
 
40
- def build_node(native_node)
41
- ::Capybara::Selenium::ChromeNode.new(self, native_node)
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
+
84
+ def delete_all_cookies
85
+ execute_cdp('Network.clearBrowserCookies')
86
+ rescue *cdp_unsupported_errors
87
+ # If the CDP clear isn't supported do original limited clear
88
+ super
89
+ end
90
+
91
+ def cdp_unsupported_errors
92
+ @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError]
93
+ end
94
+
95
+ def execute_cdp(cmd, params = {})
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
103
+ end
104
+
105
+ def build_node(native_node, initial_cache = {})
106
+ ::Capybara::Selenium::ChromeNode.new(self, native_node, initial_cache)
107
+ end
108
+
109
+ def chromedriver_version
110
+ @chromedriver_version ||= begin
111
+ caps = browser.capabilities
112
+ caps['chrome']&.fetch('chromedriverVersion', nil).to_f
113
+ end
42
114
  end
43
115
  end
116
+
117
+ Capybara::Selenium::Driver.register_specialization :chrome, Capybara::Selenium::Driver::ChromeDriver