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
@@ -7,8 +7,8 @@ module Capybara::Selenium::Driver::ChromeDriver
7
7
  within_given_window(handle) do
8
8
  begin
9
9
  super
10
- rescue NoMethodError => err
11
- raise unless err.message =~ /full_screen_window/
10
+ rescue NoMethodError => e
11
+ raise unless e.message.match?(/full_screen_window/)
12
12
 
13
13
  result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
14
14
  result['value']
@@ -19,7 +19,7 @@ module Capybara::Selenium::Driver::ChromeDriver
19
19
  def resize_window_to(handle, width, height)
20
20
  super
21
21
  rescue Selenium::WebDriver::Error::UnknownError => err
22
- raise unless err.message =~ /failed to change window state/
22
+ raise unless err.message.match?(/failed to change window state/)
23
23
 
24
24
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
25
25
  # and raises unnecessary error. Wait a bit and try again.
@@ -40,11 +40,21 @@ private
40
40
 
41
41
  def delete_all_cookies
42
42
  execute_cdp('Network.clearBrowserCookies')
43
- rescue Selenium::WebDriver::Error::UnhandledError, Selenium::WebDriver::Error::WebDriverError
43
+ rescue *cdp_unsupported_errors
44
44
  # If the CDP clear isn't supported do original limited clear
45
45
  super
46
46
  end
47
47
 
48
+ def cdp_unsupported_errors
49
+ @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError].tap do |errors|
50
+ unless selenium_4?
51
+ ::Selenium::WebDriver.logger.suppress_deprecations do
52
+ errors << Selenium::WebDriver::Error::UnhandledError
53
+ end
54
+ end
55
+ end
56
+ end
57
+
48
58
  def execute_cdp(cmd, params = {})
49
59
  args = { cmd: cmd, params: params }
50
60
  result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
@@ -59,3 +69,5 @@ private
59
69
  browser.send(:bridge)
60
70
  end
61
71
  end
72
+
73
+ Capybara::Selenium::Driver.register_specialization :chrome, Capybara::Selenium::Driver::ChromeDriver
@@ -3,6 +3,27 @@
3
3
  require 'capybara/selenium/nodes/firefox_node'
4
4
 
5
5
  module Capybara::Selenium::Driver::FirefoxDriver
6
+ def self.extended(driver)
7
+ driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver if w3c?(driver)
8
+ end
9
+
10
+ def self.w3c?(driver)
11
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
12
+ driver.browser.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
13
+ end
14
+ end
15
+
16
+ module Capybara::Selenium::Driver::W3CFirefoxDriver
17
+ class << self
18
+ def extended(driver)
19
+ require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(driver.browser)
20
+ end
21
+
22
+ def pause_broken?(sel_driver)
23
+ sel_driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
24
+ end
25
+ end
26
+
6
27
  def resize_window_to(handle, width, height)
7
28
  within_given_window(handle) do
8
29
  # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
@@ -48,3 +69,5 @@ private
48
69
  ::Capybara::Selenium::FirefoxNode.new(self, native_node, initial_cache)
49
70
  end
50
71
  end
72
+
73
+ Capybara::Selenium::Driver.register_specialization :firefox, Capybara::Selenium::Driver::FirefoxDriver
@@ -11,3 +11,8 @@ module Capybara::Selenium::Driver::InternetExplorerDriver
11
11
  handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
12
12
  end
13
13
  end
14
+
15
+ module Capybara::Selenium
16
+ Driver.register_specialization :ie, Driver::InternetExplorerDriver
17
+ Driver.register_specialization :internet_explorer, Driver::InternetExplorerDriver
18
+ end
@@ -3,7 +3,17 @@
3
3
  require 'capybara/selenium/nodes/safari_node'
4
4
 
5
5
  module Capybara::Selenium::Driver::SafariDriver
6
- private # rubocop:disable Layout/IndentationWidth
6
+ def switch_to_frame(frame)
7
+ return super unless frame == :parent
8
+
9
+ # safaridriver/safari has an issue where switch_to_frame(:parent)
10
+ # behaves like switch_to_frame(:top)
11
+ handles = @frame_handles[current_window_handle]
12
+ browser.switch_to.default_content
13
+ handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
14
+ end
15
+
16
+ private
7
17
 
8
18
  def build_node(native_node, initial_cache = {})
9
19
  ::Capybara::Selenium::SafariNode.new(self, native_node, initial_cache)
@@ -13,3 +23,6 @@ private # rubocop:disable Layout/IndentationWidth
13
23
  browser.send(:bridge)
14
24
  end
15
25
  end
26
+
27
+ Capybara::Selenium::Driver.register_specialization(/^(safari|Safari_Technology_Preview)$/,
28
+ Capybara::Selenium::Driver::SafariDriver)
@@ -18,44 +18,22 @@ module Capybara
18
18
  hints = []
19
19
 
20
20
  if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
21
- els = filter_by_text(els, texts) unless texts.empty?
21
+ begin
22
+ els = filter_by_text(els, texts) unless texts.empty?
23
+ hints_js, functions = build_hints_js(uses_visibility, styles)
22
24
 
23
- hints_js = +''
24
- functions = []
25
- if uses_visibility && !is_displayed_atom.empty?
26
- hints_js << <<~VISIBILITY_JS
27
- var vis_func = #{is_displayed_atom};
28
- VISIBILITY_JS
29
- functions << 'vis_func'
30
- end
31
-
32
- if styles.is_a? Hash
33
- hints_js << <<~STYLE_JS
34
- var style_func = function(el){
35
- var el_styles = window.getComputedStyle(el);
36
- return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
37
- res[style] = el_styles[style];
38
- return res;
39
- }, {});
40
- };
41
- STYLE_JS
42
- functions << 'style_func'
43
- end
44
-
45
- unless functions.empty?
46
- hints_js << <<~EACH_JS
47
- return arguments[0].map(function(el){
48
- return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
49
- });
50
- EACH_JS
51
-
52
- hints = es_context.execute_script hints_js, els
53
- hints.map! do |results|
54
- result = {}
55
- result[:style] = results.pop if styles.is_a? Hash
56
- result[:visible] = results.pop if uses_visibility
57
- result
25
+ unless functions.empty?
26
+ hints = es_context.execute_script(hints_js, els).map! do |results|
27
+ hint = {}
28
+ hint[:style] = results.pop if functions.include?(:style_func)
29
+ hint[:visible] = results.pop if functions.include?(:vis_func)
30
+ hint
31
+ end
58
32
  end
33
+ rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
34
+ ::Capybara::NotSupportedByDriverError
35
+ # warn 'Unexpected Stale Element Error - skipping optimization'
36
+ hints = []
59
37
  end
60
38
  end
61
39
  els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
@@ -63,7 +41,7 @@ module Capybara
63
41
 
64
42
  def filter_by_text(elements, texts)
65
43
  es_context.execute_script <<~JS, elements, texts
66
- var texts = arguments[1]
44
+ var texts = arguments[1];
67
45
  return arguments[0].filter(function(el){
68
46
  var content = el.textContent.toLowerCase();
69
47
  return texts.every(function(txt){ return content.indexOf(txt.toLowerCase()) != -1 });
@@ -71,6 +49,39 @@ module Capybara
71
49
  JS
72
50
  end
73
51
 
52
+ def build_hints_js(uses_visibility, styles)
53
+ functions = []
54
+ hints_js = +''
55
+
56
+ if uses_visibility && !is_displayed_atom.empty?
57
+ hints_js << <<~VISIBILITY_JS
58
+ var vis_func = #{is_displayed_atom};
59
+ VISIBILITY_JS
60
+ functions << :vis_func
61
+ end
62
+
63
+ if styles.is_a? Hash
64
+ hints_js << <<~STYLE_JS
65
+ var style_func = function(el){
66
+ var el_styles = window.getComputedStyle(el);
67
+ return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
68
+ res[style] = el_styles[style];
69
+ return res;
70
+ }, {});
71
+ };
72
+ STYLE_JS
73
+ functions << :style_func
74
+ end
75
+
76
+ hints_js << <<~EACH_JS
77
+ return arguments[0].map(function(el){
78
+ return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
79
+ });
80
+ EACH_JS
81
+
82
+ [hints_js, functions]
83
+ end
84
+
74
85
  def es_context
75
86
  respond_to?(:execute_script) ? self : driver
76
87
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module DeprecationSuppressor
6
+ def deprecate(*)
7
+ super unless @suppress_for_capybara
8
+ end
9
+
10
+ def suppress_deprecations
11
+ prev_suppress_for_capybara, @suppress_for_capybara = @suppress_for_capybara, true
12
+ yield
13
+ ensure
14
+ @suppress_for_capybara = prev_suppress_for_capybara
15
+ end
16
+ end
17
+
18
+ module ErrorSuppressor
19
+ def for_code(*)
20
+ ::Selenium::WebDriver.logger.suppress_deprecations do
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ Selenium::WebDriver::Logger.prepend Capybara::Selenium::DeprecationSuppressor
29
+ Selenium::WebDriver::Error.singleton_class.prepend Capybara::Selenium::ErrorSuppressor
@@ -56,9 +56,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
56
56
  def set(value, **options)
57
57
  raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}" if value.is_a?(Array) && !multiple?
58
58
 
59
- case tag_name
59
+ tag_name, type = attrs(:tagName, :type)
60
+ case tag_name.downcase
60
61
  when 'input'
61
- case self[:type]
62
+ case type.downcase
62
63
  when 'radio'
63
64
  click
64
65
  when 'checkbox'
@@ -96,13 +97,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
96
97
  return native.click if click_options.empty?
97
98
 
98
99
  click_with_options(click_options)
99
- rescue StandardError => err
100
- if err.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
101
- err.message =~ /Other element would receive the click/
100
+ rescue StandardError => e
101
+ if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
102
+ e.message.match?(/Other element would receive the click/)
102
103
  scroll_to_center
103
104
  end
104
105
 
105
- raise err
106
+ raise e
106
107
  end
107
108
 
108
109
  def right_click(keys = [], **options)
@@ -148,7 +149,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
148
149
  return true unless native.enabled?
149
150
 
150
151
  # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
151
- tag_name == 'fieldset' && find_xpath('ancestor-or-self::fieldset[@disabled]').any?
152
+ find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
152
153
  end
153
154
 
154
155
  def content_editable?
@@ -211,7 +212,7 @@ private
211
212
  # Clear field by JavaScript assignment of the value property.
212
213
  # Script can change a readonly element which user input cannot, so
213
214
  # don't execute if readonly.
214
- driver.execute_script "arguments[0].value = ''", self unless clear == :none
215
+ driver.execute_script "if (!arguments[0].readOnly){ arguments[0].value = '' }", self unless clear == :none
215
216
  send_keys(value)
216
217
  end
217
218
  end
@@ -269,6 +270,7 @@ private
269
270
 
270
271
  def update_value_js(value)
271
272
  driver.execute_script(<<-JS, self, value)
273
+ if (arguments[0].readOnly) { return };
272
274
  if (document.activeElement !== arguments[0]){
273
275
  arguments[0].focus();
274
276
  }
@@ -352,6 +354,18 @@ private
352
354
  self.class.new(driver, native_node, initial_cache)
353
355
  end
354
356
 
357
+ def attrs(*attr_names)
358
+ return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
359
+
360
+ driver.evaluate_script <<~'JS', self, attr_names.map(&:to_s)
361
+ (function(el, names){
362
+ return names.map(function(name){
363
+ return el[name]
364
+ });
365
+ })(arguments[0], arguments[1]);
366
+ JS
367
+ end
368
+
355
369
  GET_XPATH_SCRIPT = <<~'JS'
356
370
  (function(el, xml){
357
371
  var xpath = '';
@@ -14,8 +14,8 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
14
14
 
15
15
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
16
16
  super(value)
17
- rescue ::Selenium::WebDriver::Error::ExpectedError => err
18
- raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if err.message.match?(/File not found : .+\n.+/m)
17
+ rescue *file_errors => e
18
+ raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if e.message.match?(/File not found : .+\n.+/m)
19
19
 
20
20
  raise
21
21
  end
@@ -28,6 +28,12 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
28
28
 
29
29
  private
30
30
 
31
+ def file_errors
32
+ @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do
33
+ [::Selenium::WebDriver::Error::ExpectedError]
34
+ end
35
+ end
36
+
31
37
  def bridge
32
38
  driver.browser.send(:bridge)
33
39
  end
@@ -36,7 +36,7 @@ module Capybara
36
36
  attr_reader :disable_markup
37
37
 
38
38
  def html_content?
39
- !!(@headers['Content-Type'] =~ /html/)
39
+ /html/.match?(@headers['Content-Type'])
40
40
  end
41
41
 
42
42
  def insert_disable(html)
@@ -12,7 +12,7 @@ module Capybara
12
12
 
13
13
  def request(&block)
14
14
  ssl? ? https_request(&block) : http_request(&block)
15
- rescue *TRY_HTTPS_ERRORS
15
+ rescue *TRY_HTTPS_ERRORS # rubocop:disable Naming/RescuedExceptionsVariableName
16
16
  res = https_request(&block)
17
17
  @ssl = true
18
18
  res
@@ -46,9 +46,9 @@ module Capybara
46
46
  @counter.increment
47
47
  begin
48
48
  @extended_app.call(env)
49
- rescue *@server_errors => err
50
- @error ||= err
51
- raise err
49
+ rescue *@server_errors => e
50
+ @error ||= e
51
+ raise e
52
52
  ensure
53
53
  @counter.decrement
54
54
  end
@@ -77,24 +77,18 @@ module Capybara
77
77
 
78
78
  remove_method :app_host=
79
79
  def app_host=(url)
80
- raise ArgumentError, "Capybara.app_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." if url && url !~ URI::DEFAULT_PARSER.make_regexp
80
+ raise ArgumentError, "Capybara.app_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
81
81
 
82
82
  @app_host = url
83
83
  end
84
84
 
85
85
  remove_method :default_host=
86
86
  def default_host=(url)
87
- raise ArgumentError, "Capybara.default_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." if url && url !~ URI::DEFAULT_PARSER.make_regexp
87
+ raise ArgumentError, "Capybara.default_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
88
88
 
89
89
  @default_host = url
90
90
  end
91
91
 
92
- remove_method :disable_animation=
93
- def disable_animation=(bool_or_allowlist)
94
- warn 'Capybara.disable_animation is a beta feature - it may change/disappear in a future point version' if bool_or_allowlist
95
- @disable_animation = bool_or_allowlist
96
- end
97
-
98
92
  remove_method :test_id=
99
93
  ##
100
94
  #
@@ -121,7 +121,7 @@ Capybara::SpecHelper.spec '#attach_file' do
121
121
 
122
122
  context "with a locator that doesn't exist" do
123
123
  it 'should raise an error' do
124
- msg = 'Unable to find file field "does not exist"'
124
+ msg = /Unable to find file field "does not exist"/
125
125
  expect do
126
126
  @session.attach_file('does not exist', with_os_path_separators(test_file_path))
127
127
  end.to raise_error(Capybara::ElementNotFound, msg)
@@ -83,7 +83,7 @@ Capybara::SpecHelper.spec '#check' do
83
83
 
84
84
  context "with a locator that doesn't exist" do
85
85
  it 'should raise an error' do
86
- msg = 'Unable to find checkbox "does not exist"'
86
+ msg = /Unable to find checkbox "does not exist"/
87
87
  expect do
88
88
  @session.check('does not exist')
89
89
  end.to raise_error(Capybara::ElementNotFound, msg)
@@ -171,11 +171,11 @@ Capybara::SpecHelper.spec '#check' do
171
171
  end
172
172
 
173
173
  it 'should raise original error when no label available' do
174
- expect { @session.check('form_cars_ariel') }.to raise_error(Capybara::ElementNotFound, 'Unable to find visible checkbox "form_cars_ariel"')
174
+ expect { @session.check('form_cars_ariel') }.to raise_error(Capybara::ElementNotFound, /Unable to find visible checkbox "form_cars_ariel"/)
175
175
  end
176
176
 
177
177
  it 'should raise error if not allowed to click label' do
178
- expect { @session.check('form_cars_mclaren', allow_label_click: false) }.to raise_error(Capybara::ElementNotFound, 'Unable to find visible checkbox "form_cars_mclaren"')
178
+ expect { @session.check('form_cars_mclaren', allow_label_click: false) }.to raise_error(Capybara::ElementNotFound, /Unable to find visible checkbox "form_cars_mclaren"/)
179
179
  end
180
180
  end
181
181
 
@@ -187,7 +187,7 @@ Capybara::SpecHelper.spec '#check' do
187
187
  end
188
188
 
189
189
  it 'should raise error if checkbox not visible' do
190
- expect { @session.check('form_cars_mclaren') }.to raise_error(Capybara::ElementNotFound, 'Unable to find visible checkbox "form_cars_mclaren"')
190
+ expect { @session.check('form_cars_mclaren') }.to raise_error(Capybara::ElementNotFound, /Unable to find visible checkbox "form_cars_mclaren"/)
191
191
  end
192
192
 
193
193
  it 'should include node filter in error if verified' do