capybara 3.16.2 → 3.17.0

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