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
@@ -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