capybara 3.16.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +321 -0
  4. data/README.md +51 -60
  5. data/lib/capybara.rb +71 -114
  6. data/lib/capybara/config.rb +8 -5
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/node.rb +15 -3
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +5 -3
  11. data/lib/capybara/minitest.rb +242 -141
  12. data/lib/capybara/minitest/spec.rb +159 -90
  13. data/lib/capybara/node/actions.rb +85 -74
  14. data/lib/capybara/node/base.rb +4 -4
  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 +216 -117
  18. data/lib/capybara/node/finders.rb +65 -65
  19. data/lib/capybara/node/matchers.rb +228 -126
  20. data/lib/capybara/node/simple.rb +9 -4
  21. data/lib/capybara/queries/ancestor_query.rb +5 -7
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +1 -1
  24. data/lib/capybara/queries/selector_query.rb +296 -30
  25. data/lib/capybara/queries/sibling_query.rb +5 -4
  26. data/lib/capybara/queries/style_query.rb +2 -2
  27. data/lib/capybara/queries/text_query.rb +13 -1
  28. data/lib/capybara/queries/title_query.rb +1 -1
  29. data/lib/capybara/rack_test/browser.rb +7 -2
  30. data/lib/capybara/rack_test/driver.rb +1 -1
  31. data/lib/capybara/rack_test/form.rb +1 -1
  32. data/lib/capybara/rack_test/node.rb +43 -7
  33. data/lib/capybara/registration_container.rb +44 -0
  34. data/lib/capybara/registrations/drivers.rb +36 -0
  35. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  36. data/lib/capybara/registrations/servers.rb +44 -0
  37. data/lib/capybara/result.rb +36 -8
  38. data/lib/capybara/rspec/matcher_proxies.rb +6 -4
  39. data/lib/capybara/rspec/matchers.rb +100 -63
  40. data/lib/capybara/rspec/matchers/base.rb +23 -10
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  47. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  48. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  49. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +219 -588
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  54. data/lib/capybara/selector/css.rb +4 -2
  55. data/lib/capybara/selector/definition.rb +277 -0
  56. data/lib/capybara/selector/definition/button.rb +52 -0
  57. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  58. data/lib/capybara/selector/definition/css.rb +10 -0
  59. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  60. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  61. data/lib/capybara/selector/definition/element.rb +27 -0
  62. data/lib/capybara/selector/definition/field.rb +40 -0
  63. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  64. data/lib/capybara/selector/definition/file_field.rb +13 -0
  65. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  66. data/lib/capybara/selector/definition/frame.rb +17 -0
  67. data/lib/capybara/selector/definition/id.rb +6 -0
  68. data/lib/capybara/selector/definition/label.rb +62 -0
  69. data/lib/capybara/selector/definition/link.rb +54 -0
  70. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  71. data/lib/capybara/selector/definition/option.rb +27 -0
  72. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  73. data/lib/capybara/selector/definition/select.rb +81 -0
  74. data/lib/capybara/selector/definition/table.rb +109 -0
  75. data/lib/capybara/selector/definition/table_row.rb +21 -0
  76. data/lib/capybara/selector/definition/xpath.rb +5 -0
  77. data/lib/capybara/selector/filter_set.rb +13 -9
  78. data/lib/capybara/selector/filters/base.rb +11 -2
  79. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  80. data/lib/capybara/selector/regexp_disassembler.rb +9 -2
  81. data/lib/capybara/selector/selector.rb +43 -448
  82. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  83. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  84. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  85. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  86. data/lib/capybara/selenium/driver.rb +125 -56
  87. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +73 -17
  88. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  89. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +41 -2
  90. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  91. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +14 -5
  92. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  93. data/lib/capybara/selenium/extensions/find.rb +67 -45
  94. data/lib/capybara/selenium/extensions/html5_drag.rb +152 -36
  95. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  96. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  97. data/lib/capybara/selenium/node.rb +227 -56
  98. data/lib/capybara/selenium/nodes/chrome_node.rb +93 -8
  99. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  100. data/lib/capybara/selenium/nodes/firefox_node.rb +37 -59
  101. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  102. data/lib/capybara/selenium/nodes/safari_node.rb +27 -54
  103. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  104. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  105. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  106. data/lib/capybara/selenium/patches/logs.rb +45 -0
  107. data/lib/capybara/server.rb +19 -3
  108. data/lib/capybara/server/animation_disabler.rb +2 -2
  109. data/lib/capybara/server/checker.rb +6 -2
  110. data/lib/capybara/server/middleware.rb +23 -13
  111. data/lib/capybara/session.rb +124 -106
  112. data/lib/capybara/session/config.rb +12 -10
  113. data/lib/capybara/session/matchers.rb +6 -6
  114. data/lib/capybara/spec/public/offset.js +6 -0
  115. data/lib/capybara/spec/public/test.js +94 -5
  116. data/lib/capybara/spec/session/all_spec.rb +84 -6
  117. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  118. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  119. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  120. data/lib/capybara/spec/session/attach_file_spec.rb +14 -6
  121. data/lib/capybara/spec/session/check_spec.rb +10 -4
  122. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  123. data/lib/capybara/spec/session/click_button_spec.rb +44 -1
  124. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  125. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  126. data/lib/capybara/spec/session/fill_in_spec.rb +37 -2
  127. data/lib/capybara/spec/session/find_spec.rb +60 -6
  128. data/lib/capybara/spec/session/first_spec.rb +1 -1
  129. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  130. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  131. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  132. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  133. data/lib/capybara/spec/session/has_css_spec.rb +35 -6
  134. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  135. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  136. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  137. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  138. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  139. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  140. data/lib/capybara/spec/session/has_text_spec.rb +47 -0
  141. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  142. data/lib/capybara/spec/session/node_spec.rb +574 -16
  143. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  144. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  145. data/lib/capybara/spec/session/scroll_spec.rb +1 -1
  146. data/lib/capybara/spec/session/select_spec.rb +5 -10
  147. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  148. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  149. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  150. data/lib/capybara/spec/session/window/window_spec.rb +10 -9
  151. data/lib/capybara/spec/spec_helper.rb +7 -2
  152. data/lib/capybara/spec/test_app.rb +26 -21
  153. data/lib/capybara/spec/views/animated.erb +49 -0
  154. data/lib/capybara/spec/views/form.erb +25 -4
  155. data/lib/capybara/spec/views/frame_child.erb +2 -1
  156. data/lib/capybara/spec/views/frame_one.erb +1 -0
  157. data/lib/capybara/spec/views/obscured.erb +9 -9
  158. data/lib/capybara/spec/views/offset.erb +32 -0
  159. data/lib/capybara/spec/views/react.erb +45 -0
  160. data/lib/capybara/spec/views/spatial.erb +31 -0
  161. data/lib/capybara/spec/views/with_animation.erb +29 -1
  162. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  163. data/lib/capybara/spec/views/with_html.erb +28 -2
  164. data/lib/capybara/spec/views/with_js.erb +2 -1
  165. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  166. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  167. data/lib/capybara/version.rb +1 -1
  168. data/lib/capybara/window.rb +10 -10
  169. data/spec/basic_node_spec.rb +6 -6
  170. data/spec/capybara_spec.rb +28 -28
  171. data/spec/dsl_spec.rb +16 -3
  172. data/spec/filter_set_spec.rb +5 -5
  173. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  174. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  175. data/spec/minitest_spec.rb +12 -2
  176. data/spec/minitest_spec_spec.rb +56 -45
  177. data/spec/rack_test_spec.rb +25 -12
  178. data/spec/regexp_dissassembler_spec.rb +53 -39
  179. data/spec/result_spec.rb +50 -54
  180. data/spec/rspec/features_spec.rb +1 -0
  181. data/spec/rspec/shared_spec_matchers.rb +78 -62
  182. data/spec/rspec_spec.rb +5 -5
  183. data/spec/sauce_spec_chrome.rb +1 -0
  184. data/spec/selector_spec.rb +26 -16
  185. data/spec/selenium_spec_chrome.rb +84 -5
  186. data/spec/selenium_spec_chrome_remote.rb +23 -8
  187. data/spec/selenium_spec_edge.rb +23 -8
  188. data/spec/selenium_spec_firefox.rb +16 -21
  189. data/spec/selenium_spec_firefox_remote.rb +4 -13
  190. data/spec/selenium_spec_ie.rb +23 -15
  191. data/spec/selenium_spec_safari.rb +17 -17
  192. data/spec/server_spec.rb +87 -42
  193. data/spec/session_spec.rb +11 -4
  194. data/spec/shared_selenium_node.rb +83 -0
  195. data/spec/shared_selenium_session.rb +62 -72
  196. data/spec/spec_helper.rb +43 -5
  197. metadata +114 -16
@@ -11,20 +11,35 @@ 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
15
  attr_reader :app, :options
16
16
 
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/
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
22
32
 
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."
33
+ attr_reader :specializations
34
+
35
+ def register_specialization(browser_name, specialization)
36
+ @specializations ||= {}
37
+ @specializations[browser_name] = specialization
38
+ end
24
39
  end
25
40
 
26
41
  def browser
27
- @browser ||= begin
42
+ unless @browser
28
43
  options[:http_client] ||= begin
29
44
  require 'capybara/selenium/patches/persistent_client'
30
45
  if options[:timeout]
@@ -34,10 +49,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
34
49
  end
35
50
  end
36
51
  processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
37
- Selenium::WebDriver.for(options[:browser], processed_options).tap do |driver|
38
- specialize_driver(driver)
39
- setup_exit_handler
40
- end
52
+ @browser = Selenium::WebDriver.for(options[:browser], processed_options)
53
+
54
+ specialize_driver
55
+ setup_exit_handler
41
56
  end
42
57
  @browser
43
58
  end
@@ -70,6 +85,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
70
85
 
71
86
  def html
72
87
  browser.page_source
88
+ rescue Selenium::WebDriver::Error::JavascriptError => e
89
+ raise unless e.message.match?(/documentElement is null/)
73
90
  end
74
91
 
75
92
  def title
@@ -115,7 +132,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
115
132
  navigated = true
116
133
  # Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
117
134
  wait_for_empty_page(timer)
118
- rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
135
+ rescue *unhandled_alert_errors
119
136
  # This error is thrown if an unhandled alert is on the page
120
137
  # Firefox appears to automatically dismiss this alert, chrome does not
121
138
  # We'll try to accept it
@@ -125,6 +142,18 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
125
142
  end
126
143
  end
127
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
+
128
157
  def switch_to_frame(frame)
129
158
  handles = @frame_handles[current_window_handle]
130
159
  case frame
@@ -135,7 +164,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
135
164
  handles.pop
136
165
  browser.switch_to.parent_frame
137
166
  else
138
- handles << frame.native
167
+ handles << frame
139
168
  browser.switch_to.frame(frame.native)
140
169
  end
141
170
  end
@@ -195,7 +224,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
195
224
 
196
225
  def accept_modal(_type, **options)
197
226
  yield if block_given?
198
- modal = find_modal(options)
227
+ modal = find_modal(**options)
199
228
 
200
229
  modal.send_keys options[:with] if options[:with]
201
230
 
@@ -206,7 +235,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
206
235
 
207
236
  def dismiss_modal(_type, **options)
208
237
  yield if block_given?
209
- modal = find_modal(options)
238
+ modal = find_modal(**options)
210
239
  message = modal.text
211
240
  modal.dismiss
212
241
  message
@@ -214,31 +243,39 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
214
243
 
215
244
  def quit
216
245
  @browser&.quit
217
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/HandleExceptions
246
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
218
247
  # Browser must have already gone
219
- rescue Selenium::WebDriver::Error::UnknownError => err
220
- 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
221
250
  # probably already gone but not sure - so warn
222
- warn "Ignoring Selenium UnknownError during driver quit: #{err.message}"
251
+ warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
223
252
  end
224
253
  ensure
225
254
  @browser = nil
226
255
  end
227
256
 
228
257
  def invalid_element_errors
229
- [
230
- ::Selenium::WebDriver::Error::StaleElementReferenceError,
231
- ::Selenium::WebDriver::Error::UnhandledError,
232
- ::Selenium::WebDriver::Error::ElementNotVisibleError,
233
- ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a chromedriver go_back/go_forward race condition
234
- ::Selenium::WebDriver::Error::ElementNotInteractableError,
235
- ::Selenium::WebDriver::Error::ElementClickInterceptedError,
236
- ::Selenium::WebDriver::Error::InvalidElementStateError,
237
- ::Selenium::WebDriver::Error::ElementNotSelectableError,
238
- ::Selenium::WebDriver::Error::ElementNotSelectableError,
239
- ::Selenium::WebDriver::Error::NoSuchElementError, # IE
240
- ::Selenium::WebDriver::Error::InvalidArgumentError # IE
241
- ]
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
242
279
  end
243
280
 
244
281
  def no_such_window_error
@@ -247,6 +284,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
247
284
 
248
285
  private
249
286
 
287
+ def selenium_4?
288
+ defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)
289
+ end
290
+
250
291
  def native_args(args)
251
292
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
252
293
  end
@@ -254,12 +295,23 @@ private
254
295
  def clear_browser_state
255
296
  delete_all_cookies
256
297
  clear_storage
257
- rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
298
+ rescue *clear_browser_state_errors
258
299
  # delete_all_cookies fails when we've previously gone
259
300
  # to about:blank, so we rescue this error and do nothing
260
301
  # instead.
261
302
  end
262
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
+
263
315
  def delete_all_cookies
264
316
  @browser.manage.delete_all_cookies
265
317
  end
@@ -267,7 +319,7 @@ private
267
319
  def clear_storage
268
320
  clear_session_storage unless options[:clear_session_storage] == false
269
321
  clear_local_storage unless options[:clear_local_storage] == false
270
- rescue Selenium::WebDriver::Error::JavascriptError # rubocop:disable Lint/HandleExceptions
322
+ rescue Selenium::WebDriver::Error::JavascriptError
271
323
  # session/local storage may not be available if on non-http pages (e.g. about:blank)
272
324
  end
273
325
 
@@ -278,7 +330,9 @@ private
278
330
  begin
279
331
  @browser&.execute_script('window.sessionStorage.clear()')
280
332
  rescue # rubocop:disable Style/RescueStandardError
281
- warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
333
+ unless options[:clear_session_storage].nil?
334
+ warn 'sessionStorage clear requested but is not supported by this driver'
335
+ end
282
336
  end
283
337
  end
284
338
  end
@@ -290,7 +344,9 @@ private
290
344
  begin
291
345
  @browser&.execute_script('window.localStorage.clear()')
292
346
  rescue # rubocop:disable Style/RescueStandardError
293
- warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
347
+ unless options[:clear_local_storage].nil?
348
+ warn 'localStorage clear requested but is not supported by this driver'
349
+ end
294
350
  end
295
351
  end
296
352
  end
@@ -299,7 +355,7 @@ private
299
355
  @browser.navigate.to(url)
300
356
  sleep 0.1 # slight wait for alert
301
357
  @browser.switch_to.alert.accept
302
- rescue modal_error # rubocop:disable Lint/HandleExceptions
358
+ rescue modal_error
303
359
  # alert now gone, should mean navigation happened
304
360
  end
305
361
 
@@ -329,16 +385,35 @@ private
329
385
  begin
330
386
  wait.until do
331
387
  alert = @browser.switch_to.alert
332
- regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
333
- 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
334
395
  end
335
- rescue Selenium::WebDriver::Error::TimeOutError
396
+ rescue *find_modal_errors
336
397
  raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
337
398
  end
338
399
  end
339
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
+
340
415
  def silenced_unknown_error_message?(msg)
341
- silenced_unknown_error_messages.any? { |regex| msg =~ regex }
416
+ silenced_unknown_error_messages.any? { |regex| msg.match? regex }
342
417
  end
343
418
 
344
419
  def silenced_unknown_error_messages
@@ -366,22 +441,15 @@ private
366
441
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
367
442
  end
368
443
 
369
- def specialize_driver(sel_driver)
370
- case sel_driver.browser
371
- when :chrome
372
- extend ChromeDriver
373
- when :firefox
374
- require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(sel_driver)
375
- extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
376
- when :ie, :internet_explorer
377
- extend InternetExplorerDriver
378
- when :safari, :Safari_Technology_Preview
379
- extend SafariDriver
380
- end
444
+ def bridge
445
+ browser.send(:bridge)
381
446
  end
382
447
 
383
- def pause_broken?(driver)
384
- driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
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
452
+ end
385
453
  end
386
454
 
387
455
  def setup_exit_handler
@@ -427,3 +495,4 @@ require 'capybara/selenium/driver_specializations/chrome_driver'
427
495
  require 'capybara/selenium/driver_specializations/firefox_driver'
428
496
  require 'capybara/selenium/driver_specializations/internet_explorer_driver'
429
497
  require 'capybara/selenium/driver_specializations/safari_driver'
498
+ 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.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.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/)
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.
@@ -33,29 +39,79 @@ module Capybara::Selenium::Driver::ChromeDriver
33
39
 
34
40
  switch_to_window(window_handles.first)
35
41
  window_handles.slice(1..-1).each { |win| close_window(win) }
36
- 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)
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,124 @@
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.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.match?(/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.match?(/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..-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
+ args = { cmd: cmd, params: params }
107
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
108
+ result['value']
109
+ end
110
+
111
+ def build_node(native_node, initial_cache = {})
112
+ ::Capybara::Selenium::EdgeNode.new(self, native_node, initial_cache)
113
+ end
114
+
115
+ def edgedriver_version
116
+ @edgedriver_version ||= begin
117
+ caps = browser.capabilities
118
+ caps['chrome']&.fetch('chromedriverVersion', nil).to_f
119
+ end
120
+ end
121
+ end
122
+
123
+ Capybara::Selenium::Driver.register_specialization :edge, Capybara::Selenium::Driver::EdgeDriver
124
+ Capybara::Selenium::Driver.register_specialization :edge_chrome, Capybara::Selenium::Driver::EdgeDriver