capybara 3.16.2 → 3.17.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +13 -0
  3. data/README.md +1 -1
  4. data/lib/capybara.rb +2 -71
  5. data/lib/capybara/config.rb +1 -2
  6. data/lib/capybara/node/actions.rb +5 -5
  7. data/lib/capybara/node/base.rb +4 -4
  8. data/lib/capybara/node/element.rb +5 -5
  9. data/lib/capybara/node/finders.rb +6 -4
  10. data/lib/capybara/node/simple.rb +3 -2
  11. data/lib/capybara/queries/selector_query.rb +5 -5
  12. data/lib/capybara/queries/style_query.rb +1 -1
  13. data/lib/capybara/rack_test/form.rb +1 -1
  14. data/lib/capybara/registrations/drivers.rb +36 -0
  15. data/lib/capybara/registrations/servers.rb +38 -0
  16. data/lib/capybara/result.rb +2 -2
  17. data/lib/capybara/rspec/matcher_proxies.rb +2 -2
  18. data/lib/capybara/rspec/matchers/base.rb +2 -2
  19. data/lib/capybara/selector.rb +55 -32
  20. data/lib/capybara/selector/css.rb +1 -1
  21. data/lib/capybara/selector/filters/base.rb +1 -1
  22. data/lib/capybara/selector/selector.rb +1 -0
  23. data/lib/capybara/selenium/driver.rb +84 -43
  24. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +16 -4
  25. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +23 -0
  26. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +5 -0
  27. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +14 -1
  28. data/lib/capybara/selenium/extensions/find.rb +48 -37
  29. data/lib/capybara/selenium/logger_suppressor.rb +29 -0
  30. data/lib/capybara/selenium/node.rb +22 -8
  31. data/lib/capybara/selenium/nodes/chrome_node.rb +8 -2
  32. data/lib/capybara/server/animation_disabler.rb +1 -1
  33. data/lib/capybara/server/checker.rb +1 -1
  34. data/lib/capybara/server/middleware.rb +3 -3
  35. data/lib/capybara/session/config.rb +2 -8
  36. data/lib/capybara/spec/session/attach_file_spec.rb +1 -1
  37. data/lib/capybara/spec/session/check_spec.rb +4 -4
  38. data/lib/capybara/spec/session/choose_spec.rb +2 -2
  39. data/lib/capybara/spec/session/click_button_spec.rb +28 -1
  40. data/lib/capybara/spec/session/fill_in_spec.rb +2 -2
  41. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  42. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  43. data/lib/capybara/spec/session/node_spec.rb +18 -6
  44. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  45. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  46. data/lib/capybara/spec/views/frame_child.erb +2 -1
  47. data/lib/capybara/spec/views/react.erb +45 -0
  48. data/lib/capybara/version.rb +1 -1
  49. data/lib/capybara/window.rb +1 -1
  50. data/spec/minitest_spec_spec.rb +1 -1
  51. data/spec/result_spec.rb +10 -6
  52. data/spec/rspec/shared_spec_matchers.rb +8 -4
  53. data/spec/selector_spec.rb +4 -0
  54. data/spec/selenium_spec_safari.rb +2 -3
  55. data/spec/session_spec.rb +7 -0
  56. data/spec/shared_selenium_session.rb +14 -11
  57. data/spec/spec_helper.rb +2 -1
  58. metadata +6 -16
@@ -41,8 +41,8 @@ module Capybara
41
41
  class WrappedElementMatcher < Base
42
42
  def matches?(actual)
43
43
  element_matches?(wrap(actual))
44
- rescue Capybara::ExpectationNotMet => err
45
- @failure_message = err.message
44
+ rescue Capybara::ExpectationNotMet => e
45
+ @failure_message = e.message
46
46
  false
47
47
  end
48
48
 
@@ -7,20 +7,28 @@ Capybara::Selector::FilterSet.add(:_field) do
7
7
  node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
8
8
  node_filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
9
9
  node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
10
- node_filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
11
10
 
12
11
  expression_filter(:name) { |xpath, val| xpath[XPath.attr(:name) == val] }
13
12
  expression_filter(:placeholder) { |xpath, val| xpath[XPath.attr(:placeholder) == val] }
13
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
14
+ expression_filter(:multiple) { |xpath, val| xpath[val ? XPath.attr(:multiple) : ~XPath.attr(:multiple)] }
14
15
 
15
- describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, multiple: nil, **|
16
+ describe(:expression_filters) do |name: nil, placeholder: nil, disabled: nil, multiple: nil, **|
17
+ desc = +''
18
+ desc << ' that is not disabled' if disabled == false
19
+ desc << " with name #{name}" if name
20
+ desc << " with placeholder #{placeholder}" if placeholder
21
+ desc << ' with the multiple attribute' if multiple == true
22
+ desc << ' without the multiple attribute' if multiple == false
23
+ desc
24
+ end
25
+
26
+ describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, **|
16
27
  desc, states = +'', []
17
28
  states << 'checked' if checked || (unchecked == false)
18
29
  states << 'not checked' if unchecked || (checked == false)
19
30
  states << 'disabled' if disabled == true
20
- states << 'not disabled' if disabled == false
21
31
  desc << " that is #{states.join(' and ')}" unless states.empty?
22
- desc << ' with the multiple attribute' if multiple == true
23
- desc << ' without the multiple attribute' if multiple == false
24
32
  desc
25
33
  end
26
34
  end
@@ -37,7 +45,7 @@ end
37
45
 
38
46
  Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
39
47
  xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
40
- locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
48
+ locator_filter { |node, id| id.is_a?(Regexp) ? id.match?(node[:id]) : true }
41
49
  end
42
50
 
43
51
  Capybara.add_selector(:field, locator_type: [String, Symbol]) do
@@ -63,16 +71,13 @@ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
63
71
  node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
64
72
  node_filter(:with) do |node, with|
65
73
  val = node.value
66
- (with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
74
+ (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
67
75
  add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
68
76
  end
69
77
  end
70
78
 
71
- describe_expression_filters do |type: nil, **options|
72
- desc = +''
73
- (expression_filters.keys & options.keys).each { |ef| desc << " with #{ef} #{options[ef]}" }
74
- desc << " of type #{type.inspect}" if type
75
- desc
79
+ describe_expression_filters do |type: nil, **|
80
+ " of type #{type.inspect}" if type
76
81
  end
77
82
 
78
83
  describe_node_filters do |**options|
@@ -90,6 +95,7 @@ Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
90
95
  end
91
96
 
92
97
  node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
98
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
93
99
  end
94
100
 
95
101
  Capybara.add_selector(:link, locator_type: [String, Symbol]) do
@@ -123,26 +129,28 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
123
129
  builder(expr).add_attribute_conditions(download: download)
124
130
  end
125
131
 
126
- describe_expression_filters do |**options|
132
+ describe_expression_filters do |download: nil, **options|
127
133
  desc = +''
128
134
  if (href = options[:href])
129
135
  desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
130
136
  elsif options.key?(:href) # is nil/false specified?
131
137
  desc << ' with no href attribute'
132
138
  end
139
+ desc << " with download attribute#{" #{download}" if download.is_a? String}" if download
140
+ desc << ' without download attribute' if download == false
133
141
  desc
134
142
  end
135
143
  end
136
144
 
137
145
  Capybara.add_selector(:button, locator_type: [String, Symbol]) do
138
- xpath(:value, :title, :type) do |locator, **options|
146
+ xpath(:value, :title, :type, :name) do |locator, **options|
139
147
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
140
148
  btn_xpath = XPath.descendant(:button)
141
149
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
142
150
 
143
151
  unless locator.nil?
144
152
  locator = locator.to_s
145
- locator_matchers = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
153
+ locator_matchers = XPath.attr(:id).equals(locator) | XPath.attr(:name).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
146
154
  locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
147
155
  locator_matchers |= XPath.attr(test_id) == locator if test_id
148
156
 
@@ -155,14 +163,20 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
155
163
  image_btn_xpath = image_btn_xpath[alt_matches]
156
164
  end
157
165
 
158
- %i[value title type].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
166
+ %i[value title type name].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
159
167
  memo[find_by_attr(ef, options[ef])]
160
168
  end
161
169
  end
162
170
 
163
171
  node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
172
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
173
+
174
+ describe_expression_filters do |disabled: nil, **options|
175
+ desc = +''
176
+ desc << ' that is not disabled' if disabled == false
177
+ desc << describe_all_expression_filters(options)
178
+ end
164
179
 
165
- describe_expression_filters
166
180
  describe_node_filters do |disabled: nil, **|
167
181
  ' that is disabled' if disabled == true
168
182
  end
@@ -176,7 +190,7 @@ Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
176
190
  end.reduce(:union)
177
191
  end
178
192
 
179
- node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == 'a' || !(value ^ node.disabled?) }
193
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
180
194
 
181
195
  describe_node_filters do |disabled: nil, **|
182
196
  ' that is disabled' if disabled == true
@@ -205,12 +219,11 @@ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
205
219
 
206
220
  node_filter(:with) do |node, with|
207
221
  val = node.value
208
- (with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
222
+ (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
209
223
  add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
210
224
  end
211
225
  end
212
226
 
213
- describe_expression_filters
214
227
  describe_node_filters do |**options|
215
228
  " with value #{options[:with].to_s.inspect}" if options.key?(:with)
216
229
  end
@@ -234,7 +247,6 @@ Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
234
247
  end
235
248
  end
236
249
 
237
- describe_expression_filters
238
250
  describe_node_filters do |option: nil, **|
239
251
  " with value #{option.inspect}" if option
240
252
  end
@@ -257,7 +269,6 @@ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
257
269
  end
258
270
  end
259
271
 
260
- describe_expression_filters
261
272
  describe_node_filters do |option: nil, **|
262
273
  " with value #{option.inspect}" if option
263
274
  end
@@ -304,18 +315,18 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
304
315
  end
305
316
  end
306
317
 
307
- describe_expression_filters do |with_options: nil, **opts|
318
+ describe_expression_filters do |with_options: nil, **|
308
319
  desc = +''
309
320
  desc << " with at least options #{with_options.inspect}" if with_options
310
- desc << describe_all_expression_filters(opts)
311
321
  desc
312
322
  end
313
323
 
314
- describe_node_filters do |options: nil, selected: nil, with_selected: nil, **|
324
+ describe_node_filters do |options: nil, selected: nil, with_selected: nil, disabled: nil, **|
315
325
  desc = +''
316
326
  desc << " with options #{options.inspect}" if options
317
327
  desc << " with #{selected.inspect} selected" if selected
318
328
  desc << " with at least #{with_selected.inspect} selected" if with_selected
329
+ desc << ' which is disabled' if disabled
319
330
  desc
320
331
  end
321
332
  end
@@ -343,10 +354,9 @@ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
343
354
  end
344
355
  end
345
356
 
346
- describe_expression_filters do |with_options: nil, **opts|
357
+ describe_expression_filters do |with_options: nil, **|
347
358
  desc = +''
348
359
  desc << " with at least options #{with_options.inspect}" if with_options
349
- desc << describe_all_expression_filters(opts)
350
360
  desc
351
361
  end
352
362
 
@@ -363,11 +373,19 @@ Capybara.add_selector(:option, locator_type: [String, Symbol]) do
363
373
  end
364
374
 
365
375
  node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
376
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
377
+
366
378
  node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
367
379
 
380
+ describe_expression_filters do |disabled: nil, **options|
381
+ desc = +''
382
+ desc << ' that is not disabled' if disabled == false
383
+ (expression_filters.keys & options.keys).inject(desc) { |memo, ef| memo << " with #{ef} #{options[ef]}" }
384
+ end
385
+
368
386
  describe_node_filters do |**options|
369
387
  desc = +''
370
- desc << " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)
388
+ desc << ' that is disabled' if options[:disabled]
371
389
  desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)
372
390
  desc
373
391
  end
@@ -384,9 +402,16 @@ Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
384
402
  end
385
403
 
386
404
  node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
405
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
406
+
407
+ describe_expression_filters do |disabled: nil, **options|
408
+ desc = +''
409
+ desc << ' that is not disabled' if disabled == false
410
+ desc << describe_all_expression_filters(options)
411
+ end
387
412
 
388
413
  describe_node_filters do |**options|
389
- " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)
414
+ ' that is disabled' if options[:disabled]
390
415
  end
391
416
  end
392
417
 
@@ -400,8 +425,6 @@ Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
400
425
  end
401
426
 
402
427
  filter_set(:_field, %i[disabled multiple name])
403
-
404
- describe_expression_filters
405
428
  end
406
429
 
407
430
  Capybara.add_selector(:label, locator_type: [String, Symbol]) do
@@ -595,7 +618,7 @@ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
595
618
  node_filter(:attributes, matcher: /.+/) do |node, name, val|
596
619
  next true unless val.is_a?(Regexp)
597
620
 
598
- (node[name] =~ val).tap do |res|
621
+ (val.match? node[name]).tap do |res|
599
622
  add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
600
623
  end
601
624
  end
@@ -6,7 +6,7 @@ module Capybara
6
6
  def self.escape(str)
7
7
  value = str.dup
8
8
  out = +''
9
- out << value.slice!(0...1) if value =~ /^[-_]/
9
+ out << value.slice!(0...1) if value.match?(/^[-_]/)
10
10
  out << (value[0].match?(NMSTART) ? value.slice!(0...1) : escape_char(value.slice!(0...1)))
11
11
  out << value.gsub(/[^a-zA-Z0-9_-]/) { |char| escape_char char }
12
12
  out
@@ -34,7 +34,7 @@ module Capybara
34
34
 
35
35
  def handles_option?(option_name)
36
36
  if matcher?
37
- option_name =~ @matcher
37
+ @matcher.match? option_name
38
38
  else
39
39
  @name == option_name
40
40
  end
@@ -59,6 +59,7 @@ module Capybara
59
59
  # * Locator: Matches the id, Capybara.test_id attribute, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
60
60
  # * Filters:
61
61
  # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
62
+ # * :name (String) - Matches the name attribute
62
63
  # * :title (String) — Matches the title attribute
63
64
  # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
64
65
  # * :value (String) — Matches the value of an input button
@@ -14,17 +14,27 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
14
14
  SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout].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
+ 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')
22
+ rescue LoadError => e
23
+ raise e unless e.message.match?(/selenium-webdriver/)
24
+
25
+ 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."
26
+ end
27
+
28
+ attr_reader :specializations
22
29
 
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."
30
+ def register_specialization(browser_name, specialization)
31
+ @specializations ||= {}
32
+ @specializations[browser_name] = specialization
33
+ end
24
34
  end
25
35
 
26
36
  def browser
27
- @browser ||= begin
37
+ unless @browser
28
38
  options[:http_client] ||= begin
29
39
  require 'capybara/selenium/patches/persistent_client'
30
40
  if options[:timeout]
@@ -34,10 +44,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
34
44
  end
35
45
  end
36
46
  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
47
+ @browser = Selenium::WebDriver.for(options[:browser], processed_options)
48
+
49
+ specialize_driver
50
+ setup_exit_handler
41
51
  end
42
52
  @browser
43
53
  end
@@ -115,7 +125,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
115
125
  navigated = true
116
126
  # Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
117
127
  wait_for_empty_page(timer)
118
- rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
128
+ rescue *unhandled_alert_errors
119
129
  # This error is thrown if an unhandled alert is on the page
120
130
  # Firefox appears to automatically dismiss this alert, chrome does not
121
131
  # We'll try to accept it
@@ -226,19 +236,27 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
226
236
  end
227
237
 
228
238
  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
- ]
239
+ @invalid_element_errors ||= begin
240
+ [
241
+ ::Selenium::WebDriver::Error::StaleElementReferenceError,
242
+ ::Selenium::WebDriver::Error::ElementNotInteractableError,
243
+ ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around chromedriver go_back/go_forward race condition
244
+ ::Selenium::WebDriver::Error::ElementClickInterceptedError,
245
+ ::Selenium::WebDriver::Error::NoSuchElementError, # IE
246
+ ::Selenium::WebDriver::Error::InvalidArgumentError # IE
247
+ ].tap do |errors|
248
+ unless selenium_4?
249
+ ::Selenium::WebDriver.logger.suppress_deprecations do
250
+ errors.concat [
251
+ ::Selenium::WebDriver::Error::UnhandledError,
252
+ ::Selenium::WebDriver::Error::ElementNotVisibleError,
253
+ ::Selenium::WebDriver::Error::InvalidElementStateError,
254
+ ::Selenium::WebDriver::Error::ElementNotSelectableError
255
+ ]
256
+ end
257
+ end
258
+ end
259
+ end
242
260
  end
243
261
 
244
262
  def no_such_window_error
@@ -247,6 +265,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
247
265
 
248
266
  private
249
267
 
268
+ def selenium_4?
269
+ defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)
270
+ end
271
+
250
272
  def native_args(args)
251
273
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
252
274
  end
@@ -254,12 +276,32 @@ private
254
276
  def clear_browser_state
255
277
  delete_all_cookies
256
278
  clear_storage
257
- rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
279
+ rescue *clear_browser_state_errors # rubocop:disable Lint/HandleExceptions
258
280
  # delete_all_cookies fails when we've previously gone
259
281
  # to about:blank, so we rescue this error and do nothing
260
282
  # instead.
261
283
  end
262
284
 
285
+ def clear_browser_state_errors
286
+ @clear_browser_state_errors ||= [Selenium::WebDriver::Error::UnknownError].tap do |errors|
287
+ unless selenium_4?
288
+ ::Selenium::WebDriver.logger.suppress_deprecations do
289
+ errors << Selenium::WebDriver::Error::UnhandledError
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ def unhandled_alert_errors
296
+ @unhandled_alert_errors ||= [Selenium::WebDriver::Error::UnexpectedAlertOpenError].tap do |errors|
297
+ unless selenium_4?
298
+ ::Selenium::WebDriver.logger.suppress_deprecations do
299
+ errors << Selenium::WebDriver::Error::UnhandledAlertError
300
+ end
301
+ end
302
+ end
303
+ end
304
+
263
305
  def delete_all_cookies
264
306
  @browser.manage.delete_all_cookies
265
307
  end
@@ -332,13 +374,23 @@ private
332
374
  regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
333
375
  alert.text.match?(regexp) ? alert : nil
334
376
  end
335
- rescue Selenium::WebDriver::Error::TimeOutError
377
+ rescue *find_modal_errors
336
378
  raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
337
379
  end
338
380
  end
339
381
 
382
+ def find_modal_errors
383
+ @find_modal_errors ||= [Selenium::WebDriver::Error::TimeoutError].tap do |errors|
384
+ unless selenium_4?
385
+ ::Selenium::WebDriver.logger.suppress_deprecations do
386
+ errors << Selenium::WebDriver::Error::TimeOutError
387
+ end
388
+ end
389
+ end
390
+ end
391
+
340
392
  def silenced_unknown_error_message?(msg)
341
- silenced_unknown_error_messages.any? { |regex| msg =~ regex }
393
+ silenced_unknown_error_messages.any? { |regex| msg.match? regex }
342
394
  end
343
395
 
344
396
  def silenced_unknown_error_messages
@@ -366,24 +418,13 @@ private
366
418
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
367
419
  end
368
420
 
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
421
+ def specialize_driver
422
+ browser_type = browser.browser
423
+ self.class.specializations.select { |k, _v| k === browser_type }.each_value do |specialization| # rubocop:disable Style/CaseEquality
424
+ extend specialization
380
425
  end
381
426
  end
382
427
 
383
- def pause_broken?(driver)
384
- driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
385
- end
386
-
387
428
  def setup_exit_handler
388
429
  main = Process.pid
389
430
  at_exit do