capybara 3.35.2 → 3.39.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +133 -7
  3. data/README.md +28 -12
  4. data/lib/capybara/config.rb +16 -4
  5. data/lib/capybara/driver/base.rb +4 -0
  6. data/lib/capybara/driver/node.rb +5 -1
  7. data/lib/capybara/dsl.rb +4 -10
  8. data/lib/capybara/helpers.rb +8 -13
  9. data/lib/capybara/minitest/spec.rb +2 -2
  10. data/lib/capybara/node/actions.rb +14 -9
  11. data/lib/capybara/node/base.rb +2 -1
  12. data/lib/capybara/node/document.rb +2 -2
  13. data/lib/capybara/node/element.rb +13 -2
  14. data/lib/capybara/node/finders.rb +11 -2
  15. data/lib/capybara/node/simple.rb +5 -1
  16. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  17. data/lib/capybara/queries/active_element_query.rb +18 -0
  18. data/lib/capybara/queries/ancestor_query.rb +2 -1
  19. data/lib/capybara/queries/base_query.rb +2 -2
  20. data/lib/capybara/queries/current_path_query.rb +1 -1
  21. data/lib/capybara/queries/selector_query.rb +38 -10
  22. data/lib/capybara/queries/sibling_query.rb +2 -1
  23. data/lib/capybara/queries/text_query.rb +1 -1
  24. data/lib/capybara/rack_test/browser.rb +63 -8
  25. data/lib/capybara/rack_test/driver.rb +4 -4
  26. data/lib/capybara/rack_test/form.rb +29 -7
  27. data/lib/capybara/rack_test/node.rb +28 -22
  28. data/lib/capybara/registration_container.rb +0 -3
  29. data/lib/capybara/registrations/drivers.rb +6 -6
  30. data/lib/capybara/registrations/servers.rb +30 -10
  31. data/lib/capybara/rspec/matcher_proxies.rb +6 -6
  32. data/lib/capybara/rspec/matchers/base.rb +8 -6
  33. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  34. data/lib/capybara/rspec/matchers/have_selector.rb +5 -5
  35. data/lib/capybara/rspec/matchers.rb +14 -14
  36. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  37. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  38. data/lib/capybara/selector/css.rb +1 -1
  39. data/lib/capybara/selector/definition/button.rb +9 -4
  40. data/lib/capybara/selector/definition/checkbox.rb +1 -1
  41. data/lib/capybara/selector/definition/file_field.rb +1 -1
  42. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  43. data/lib/capybara/selector/definition/link.rb +2 -1
  44. data/lib/capybara/selector/definition/radio_button.rb +1 -1
  45. data/lib/capybara/selector/definition.rb +4 -2
  46. data/lib/capybara/selector/filter_set.rb +4 -7
  47. data/lib/capybara/selector/regexp_disassembler.rb +2 -5
  48. data/lib/capybara/selector/selector.rb +5 -1
  49. data/lib/capybara/selector.rb +1 -0
  50. data/lib/capybara/selenium/driver.rb +30 -13
  51. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  52. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +9 -5
  53. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  54. data/lib/capybara/selenium/extensions/html5_drag.rb +5 -4
  55. data/lib/capybara/selenium/logger_suppressor.rb +6 -2
  56. data/lib/capybara/selenium/node.rb +82 -33
  57. data/lib/capybara/selenium/nodes/chrome_node.rb +6 -2
  58. data/lib/capybara/selenium/nodes/edge_node.rb +25 -3
  59. data/lib/capybara/selenium/nodes/firefox_node.rb +3 -3
  60. data/lib/capybara/selenium/nodes/safari_node.rb +4 -4
  61. data/lib/capybara/selenium/patches/action_pauser.rb +3 -3
  62. data/lib/capybara/selenium/patches/atoms.rb +1 -1
  63. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  64. data/lib/capybara/server/animation_disabler.rb +40 -23
  65. data/lib/capybara/server/middleware.rb +1 -1
  66. data/lib/capybara/session/config.rb +4 -2
  67. data/lib/capybara/session.rb +31 -32
  68. data/lib/capybara/spec/public/test.js +4 -0
  69. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  70. data/lib/capybara/spec/session/all_spec.rb +10 -14
  71. data/lib/capybara/spec/session/assert_text_spec.rb +17 -17
  72. data/lib/capybara/spec/session/attach_file_spec.rb +6 -0
  73. data/lib/capybara/spec/session/check_spec.rb +10 -0
  74. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  75. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  76. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  77. data/lib/capybara/spec/session/fill_in_spec.rb +6 -0
  78. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  79. data/lib/capybara/spec/session/find_spec.rb +7 -1
  80. data/lib/capybara/spec/session/first_spec.rb +1 -1
  81. data/lib/capybara/spec/session/frame/within_frame_spec.rb +2 -0
  82. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  83. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -2
  84. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  85. data/lib/capybara/spec/session/has_button_spec.rb +30 -0
  86. data/lib/capybara/spec/session/has_current_path_spec.rb +3 -3
  87. data/lib/capybara/spec/session/has_field_spec.rb +25 -1
  88. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  89. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  90. data/lib/capybara/spec/session/has_select_spec.rb +10 -4
  91. data/lib/capybara/spec/session/has_selector_spec.rb +15 -0
  92. data/lib/capybara/spec/session/has_text_spec.rb +6 -14
  93. data/lib/capybara/spec/session/matches_style_spec.rb +2 -0
  94. data/lib/capybara/spec/session/node_spec.rb +82 -1
  95. data/lib/capybara/spec/session/reset_session_spec.rb +13 -0
  96. data/lib/capybara/spec/session/scroll_spec.rb +7 -5
  97. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  98. data/lib/capybara/spec/session/window/window_spec.rb +1 -1
  99. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  100. data/lib/capybara/spec/session/within_spec.rb +13 -0
  101. data/lib/capybara/spec/spec_helper.rb +12 -5
  102. data/lib/capybara/spec/test_app.rb +91 -14
  103. data/lib/capybara/spec/views/animated.erb +1 -1
  104. data/lib/capybara/spec/views/form.erb +28 -3
  105. data/lib/capybara/spec/views/frame_child.erb +1 -1
  106. data/lib/capybara/spec/views/frame_one.erb +1 -1
  107. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  108. data/lib/capybara/spec/views/frame_two.erb +1 -1
  109. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  110. data/lib/capybara/spec/views/layout.erb +10 -0
  111. data/lib/capybara/spec/views/obscured.erb +1 -1
  112. data/lib/capybara/spec/views/offset.erb +2 -1
  113. data/lib/capybara/spec/views/path.erb +2 -2
  114. data/lib/capybara/spec/views/popup_one.erb +1 -1
  115. data/lib/capybara/spec/views/popup_two.erb +1 -1
  116. data/lib/capybara/spec/views/react.erb +2 -2
  117. data/lib/capybara/spec/views/scroll.erb +2 -1
  118. data/lib/capybara/spec/views/spatial.erb +1 -1
  119. data/lib/capybara/spec/views/with_animation.erb +2 -3
  120. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  121. data/lib/capybara/spec/views/with_dragula.erb +2 -2
  122. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  123. data/lib/capybara/spec/views/with_hover.erb +2 -2
  124. data/lib/capybara/spec/views/with_html.erb +3 -3
  125. data/lib/capybara/spec/views/with_jquery_animation.erb +1 -1
  126. data/lib/capybara/spec/views/with_js.erb +2 -3
  127. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  128. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  129. data/lib/capybara/spec/views/with_scope.erb +2 -2
  130. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  131. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  132. data/lib/capybara/spec/views/with_sortable_js.erb +2 -2
  133. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  134. data/lib/capybara/spec/views/with_windows.erb +1 -1
  135. data/lib/capybara/spec/views/within_frames.erb +1 -1
  136. data/lib/capybara/version.rb +1 -1
  137. data/lib/capybara/window.rb +1 -1
  138. data/lib/capybara.rb +23 -24
  139. data/spec/basic_node_spec.rb +16 -3
  140. data/spec/capybara_spec.rb +12 -0
  141. data/spec/counter_spec.rb +35 -0
  142. data/spec/css_builder_spec.rb +1 -1
  143. data/spec/css_splitter_spec.rb +1 -1
  144. data/spec/dsl_spec.rb +5 -3
  145. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  146. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  147. data/spec/minitest_spec.rb +4 -0
  148. data/spec/minitest_spec_spec.rb +4 -0
  149. data/spec/per_session_config_spec.rb +1 -1
  150. data/spec/rack_test_spec.rb +30 -12
  151. data/spec/result_spec.rb +32 -35
  152. data/spec/rspec/features_spec.rb +3 -3
  153. data/spec/rspec/scenarios_spec.rb +2 -2
  154. data/spec/rspec/shared_spec_matchers.rb +3 -3
  155. data/spec/rspec_matchers_spec.rb +25 -0
  156. data/spec/rspec_spec.rb +2 -2
  157. data/spec/sauce_spec_chrome.rb +4 -4
  158. data/spec/selector_spec.rb +4 -4
  159. data/spec/selenium_spec_chrome.rb +16 -16
  160. data/spec/selenium_spec_chrome_remote.rb +16 -15
  161. data/spec/selenium_spec_edge.rb +12 -6
  162. data/spec/selenium_spec_firefox.rb +24 -7
  163. data/spec/selenium_spec_firefox_remote.rb +19 -4
  164. data/spec/selenium_spec_ie.rb +7 -8
  165. data/spec/selenium_spec_safari.rb +34 -20
  166. data/spec/server_spec.rb +7 -7
  167. data/spec/shared_selenium_node.rb +0 -4
  168. data/spec/shared_selenium_session.rb +24 -14
  169. data/spec/spec_helper.rb +35 -2
  170. data/spec/whitespace_normalizer_spec.rb +54 -0
  171. data/spec/xpath_builder_spec.rb +1 -1
  172. metadata +40 -14
  173. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -5,12 +5,24 @@ Capybara.register_server :default do |app, port, _host|
5
5
  end
6
6
 
7
7
  Capybara.register_server :webrick do |app, port, host, **options|
8
- require 'rack/handler/webrick'
8
+ base_class = begin
9
+ require 'rack/handler/webrick'
10
+ Rack
11
+ rescue LoadError
12
+ # Rack 3 separated out the webrick handle - no way test currently in Capybaras automated
13
+ # tests due to Sinatra not yet supporting Rack 3 - experimental
14
+ require 'rackup/handler/webrick'
15
+ Rackup
16
+ end
9
17
  options = { Host: host, Port: port, AccessLog: [], Logger: WEBrick::Log.new(nil, 0) }.merge(options)
10
- Rack::Handler::WEBrick.run(app, **options)
18
+ base_class::Handler::WEBrick.run(app, **options)
11
19
  end
12
20
 
13
- Capybara.register_server :puma do |app, port, host, **options|
21
+ Capybara.register_server :puma do |app, port, host, **options| # rubocop:disable Metrics/BlockLength
22
+ begin
23
+ require 'rackup'
24
+ rescue LoadError # rubocop:disable Lint/SuppressedException
25
+ end
14
26
  begin
15
27
  require 'rack/handler/puma'
16
28
  rescue LoadError
@@ -29,17 +41,25 @@ Capybara.register_server :puma do |app, port, host, **options|
29
41
 
30
42
  conf = Rack::Handler::Puma.config(app, options)
31
43
  conf.clamp
32
- events = conf.options[:Silent] ? ::Puma::Events.strings : ::Puma::Events.stdio
33
44
 
34
45
  puma_ver = Gem::Version.new(Puma::Const::PUMA_VERSION)
35
46
  require_relative 'patches/puma_ssl' if Gem::Requirement.new('>=4.0.0', '< 4.1.0').satisfied_by?(puma_ver)
36
47
 
37
- events.log 'Capybara starting Puma...'
38
- events.log "* Version #{Puma::Const::PUMA_VERSION} , codename: #{Puma::Const::CODE_NAME}"
39
- events.log "* Min threads: #{conf.options[:min_threads]}, max threads: #{conf.options[:max_threads]}"
48
+ logger = (defined?(Puma::LogWriter) ? Puma::LogWriter : Puma::Events).then do |cls|
49
+ conf.options[:Silent] ? cls.strings : cls.stdio
50
+ end
51
+ conf.options[:log_writer] = logger
52
+
53
+ logger.log 'Capybara starting Puma...'
54
+ logger.log "* Version #{Puma::Const::PUMA_VERSION} , codename: #{Puma::Const::CODE_NAME}"
55
+ logger.log "* Min threads: #{conf.options[:min_threads]}, max threads: #{conf.options[:max_threads]}"
40
56
 
41
- Puma::Server.new(conf.app, events, conf.options).tap do |s|
42
- s.binder.parse conf.options[:binds], s.events
43
- s.min_threads, s.max_threads = conf.options[:min_threads], conf.options[:max_threads]
57
+ Puma::Server.new(
58
+ conf.app,
59
+ defined?(Puma::LogWriter) ? nil : logger,
60
+ conf.options
61
+ ).tap do |s|
62
+ s.binder.parse conf.options[:binds], (s.log_writer rescue s.events) # rubocop:disable Style/RescueModifier
63
+ s.min_threads, s.max_threads = conf.options[:min_threads], conf.options[:max_threads] if s.respond_to? :min_threads=
44
64
  end.run.join
45
65
  end
@@ -23,7 +23,7 @@ end
23
23
  if RUBY_ENGINE == 'jruby'
24
24
  # :nocov:
25
25
  module Capybara::DSL
26
- class <<self
26
+ class << self
27
27
  remove_method :included
28
28
 
29
29
  def included(base)
@@ -36,7 +36,7 @@ if RUBY_ENGINE == 'jruby'
36
36
  end
37
37
  end
38
38
 
39
- if defined?(::RSpec::Matchers)
39
+ if defined?(RSpec::Matchers)
40
40
  module ::RSpec::Matchers
41
41
  def self.included(base)
42
42
  base.send(:include, ::Capybara::RSpecMatcherProxies) if base.include?(::Capybara::DSL)
@@ -55,7 +55,7 @@ else
55
55
  end
56
56
 
57
57
  def self.prepended(base)
58
- class <<base
58
+ class << base
59
59
  prepend ClassMethods
60
60
  end
61
61
  end
@@ -70,13 +70,13 @@ else
70
70
  end
71
71
 
72
72
  def self.prepended(base)
73
- class <<base
73
+ class << base
74
74
  prepend ClassMethods
75
75
  end
76
76
  end
77
77
  end
78
78
 
79
- Capybara::DSL.prepend ::Capybara::DSLRSpecProxyInstaller
79
+ Capybara::DSL.prepend Capybara::DSLRSpecProxyInstaller
80
80
 
81
- ::RSpec::Matchers.prepend ::Capybara::RSpecMatcherProxyInstaller if defined?(::RSpec::Matchers)
81
+ RSpec::Matchers.prepend Capybara::RSpecMatcherProxyInstaller if defined?(RSpec::Matchers)
82
82
  end
@@ -47,14 +47,16 @@ module Capybara
47
47
  end
48
48
 
49
49
  class WrappedElementMatcher < Base
50
- def matches?(actual)
50
+ def matches?(actual, &filter_block)
51
+ @filter_block ||= filter_block
51
52
  element_matches?(wrap(actual))
52
53
  rescue Capybara::ExpectationNotMet => e
53
54
  @failure_message = e.message
54
55
  false
55
56
  end
56
57
 
57
- def does_not_match?(actual)
58
+ def does_not_match?(actual, &filter_block)
59
+ @filter_block ||= filter_block
58
60
  element_does_not_match?(wrap(actual))
59
61
  rescue Capybara::ExpectationNotMet => e
60
62
  @failure_message_when_negated = e.message
@@ -86,12 +88,12 @@ module Capybara
86
88
  @matcher = matcher
87
89
  end
88
90
 
89
- def matches?(actual)
90
- @matcher.does_not_match?(actual)
91
+ def matches?(actual, &filter_block)
92
+ @matcher.does_not_match?(actual, &filter_block)
91
93
  end
92
94
 
93
- def does_not_match?(actual)
94
- @matcher.matches?(actual)
95
+ def does_not_match?(actual, &filter_block)
96
+ @matcher.matches?(actual, &filter_block)
95
97
  end
96
98
 
97
99
  def description
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if defined?(::RSpec::Expectations::Version)
3
+ if defined?(RSpec::Expectations::Version)
4
4
  module Capybara
5
5
  module RSpecMatchers
6
6
  module Matchers
@@ -8,10 +8,10 @@ module Capybara
8
8
  class HaveSelector < CountableWrappedElementMatcher
9
9
  def initialize(*args, **kw_args, &filter_block)
10
10
  super
11
- if (RUBY_VERSION >= '2.7') && (@args.size < 2) && @kw_args.keys.any?(String) # rubocop:disable Style/GuardClause
12
- @args.push(@kw_args)
13
- @kw_args = {}
14
- end
11
+ return unless (@args.size < 2) && @kw_args.keys.any?(String)
12
+
13
+ @args.push(@kw_args)
14
+ @kw_args = {}
15
15
  end
16
16
 
17
17
  def element_matches?(el)
@@ -64,7 +64,7 @@ module Capybara
64
64
  el.assert_any_of_selectors(*@args, **session_query_options, &@filter_block)
65
65
  end
66
66
 
67
- def does_not_match?(_actual)
67
+ def does_not_match?(el)
68
68
  el.assert_none_of_selectors(*@args, **session_query_options, &@filter_block)
69
69
  end
70
70
 
@@ -15,36 +15,36 @@ module Capybara
15
15
  # RSpec matcher for whether the element(s) matching a given selector exist.
16
16
  #
17
17
  # @see Capybara::Node::Matchers#assert_selector
18
- def have_selector(*args, **kw_args, &optional_filter_block)
19
- Matchers::HaveSelector.new(*args, **kw_args, &optional_filter_block)
18
+ def have_selector(...)
19
+ Matchers::HaveSelector.new(...)
20
20
  end
21
21
 
22
22
  # RSpec matcher for whether the element(s) matching a group of selectors exist.
23
23
  #
24
24
  # @see Capybara::Node::Matchers#assert_all_of_selectors
25
- def have_all_of_selectors(*args, **kw_args, &optional_filter_block)
26
- Matchers::HaveAllSelectors.new(*args, **kw_args, &optional_filter_block)
25
+ def have_all_of_selectors(...)
26
+ Matchers::HaveAllSelectors.new(...)
27
27
  end
28
28
 
29
29
  # RSpec matcher for whether no element(s) matching a group of selectors exist.
30
30
  #
31
31
  # @see Capybara::Node::Matchers#assert_none_of_selectors
32
- def have_none_of_selectors(*args, **kw_args, &optional_filter_block)
33
- Matchers::HaveNoSelectors.new(*args, **kw_args, &optional_filter_block)
32
+ def have_none_of_selectors(...)
33
+ Matchers::HaveNoSelectors.new(...)
34
34
  end
35
35
 
36
36
  # RSpec matcher for whether the element(s) matching any of a group of selectors exist.
37
37
  #
38
38
  # @see Capybara::Node::Matchers#assert_any_of_selectors
39
- def have_any_of_selectors(*args, **kw_args, &optional_filter_block)
40
- Matchers::HaveAnySelectors.new(*args, **kw_args, &optional_filter_block)
39
+ def have_any_of_selectors(...)
40
+ Matchers::HaveAnySelectors.new(...)
41
41
  end
42
42
 
43
43
  # RSpec matcher for whether the current element matches a given selector.
44
44
  #
45
45
  # @see Capybara::Node::Matchers#assert_matches_selector
46
- def match_selector(*args, **kw_args, &optional_filter_block)
47
- Matchers::MatchSelector.new(*args, **kw_args, &optional_filter_block)
46
+ def match_selector(...)
47
+ Matchers::MatchSelector.new(...)
48
48
  end
49
49
 
50
50
  %i[css xpath].each do |selector|
@@ -177,15 +177,15 @@ module Capybara
177
177
  # RSpec matcher for whether sibling element(s) matching a given selector exist.
178
178
  #
179
179
  # @see Capybara::Node::Matchers#assert_sibling
180
- def have_sibling(*args, **kw_args, &optional_filter_block)
181
- Matchers::HaveSibling.new(*args, **kw_args, &optional_filter_block)
180
+ def have_sibling(...)
181
+ Matchers::HaveSibling.new(...)
182
182
  end
183
183
 
184
184
  # RSpec matcher for whether ancestor element(s) matching a given selector exist.
185
185
  #
186
186
  # @see Capybara::Node::Matchers#assert_ancestor
187
- def have_ancestor(*args, **kw_args, &optional_filter_block)
188
- Matchers::HaveAncestor.new(*args, **kw_args, &optional_filter_block)
187
+ def have_ancestor(...)
188
+ Matchers::HaveAncestor.new(...)
189
189
  end
190
190
 
191
191
  ##
@@ -76,7 +76,7 @@ module Capybara
76
76
  else
77
77
  cls = Array(classes).reject { |c| c.is_a? Regexp }.group_by { |cl| cl.match?(/^!(?!!!)/) }
78
78
  [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
79
- cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join]
79
+ cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..))})" }).join]
80
80
  end
81
81
  end
82
82
  end
@@ -51,7 +51,7 @@ module Capybara
51
51
  else
52
52
  Array(classes).reject { |c| c.is_a? Regexp }.map do |klass|
53
53
  if klass.match?(/^!(?!!!)/)
54
- !XPath.attr(:class).contains_word(klass.slice(1..-1))
54
+ !XPath.attr(:class).contains_word(klass.slice(1..))
55
55
  else
56
56
  XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
57
57
  end
@@ -43,7 +43,7 @@ module Capybara
43
43
  when '"', "'"
44
44
  selector << parse_string(char, str)
45
45
  when '\\'
46
- selector << char + str.getc
46
+ selector << (char + str.getc)
47
47
  when ','
48
48
  selectors << selector.strip
49
49
  selector.clear
@@ -14,16 +14,17 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
14
14
  XPath.string.n.is(locator) |
15
15
  XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
16
16
 
17
- input_btn_xpath = input_btn_xpath[locator_matchers] + locate_label(locator).descendant(input_btn_xpath)
18
- btn_xpath = btn_xpath[btn_matchers] + locate_label(locator).descendant(btn_xpath)
17
+ label_contains_xpath = locate_label(locator).descendant[labellable_elements]
18
+ input_btn_xpath = input_btn_xpath[locator_matchers]
19
+ btn_xpath = btn_xpath[btn_matchers]
19
20
  aria_btn_xpath = aria_btn_xpath[btn_matchers]
20
21
 
21
22
  alt_matches = XPath.attr(:alt).is(locator)
22
23
  alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
23
- image_btn_xpath = image_btn_xpath[alt_matches] + locate_label(locator).descendant(image_btn_xpath)
24
+ image_btn_xpath = image_btn_xpath[alt_matches]
24
25
  end
25
26
 
26
- btn_xpaths = [input_btn_xpath, btn_xpath, image_btn_xpath]
27
+ btn_xpaths = [input_btn_xpath, btn_xpath, image_btn_xpath, label_contains_xpath].compact
27
28
  btn_xpaths << aria_btn_xpath if enable_aria_role
28
29
 
29
30
  %i[value title type].inject(btn_xpaths.inject(&:union)) do |memo, ef|
@@ -60,4 +61,8 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
60
61
  (XPath.attr(config.test_id) == locator if config.test_id)
61
62
  ].compact.inject(&:|)
62
63
  end
64
+
65
+ def labellable_elements
66
+ (XPath.self(:input) & XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')) | XPath.self(:button)
67
+ end
63
68
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
4
4
  xpath do |locator, allow_self: nil, **options|
5
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
5
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
6
6
  XPath.attr(:type) == 'checkbox'
7
7
  ]
8
8
  locate_field(xpath, locator, **options)
@@ -3,7 +3,7 @@
3
3
  Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
4
4
  label 'file field'
5
5
  xpath do |locator, allow_self: nil, **options|
6
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
7
7
  XPath.attr(:type) == 'file'
8
8
  ]
9
9
  locate_field(xpath, locator, **options)
@@ -3,7 +3,7 @@
3
3
  Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
4
4
  label 'field'
5
5
  xpath do |locator, allow_self: nil, **options|
6
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input, :textarea)[
7
7
  !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
8
8
  ]
9
9
  locate_field(xpath, locator, **options)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Capybara.add_selector(:link, locator_type: [String, Symbol]) do
4
- xpath do |locator, href: true, alt: nil, title: nil, **|
4
+ xpath do |locator, href: true, alt: nil, title: nil, target: nil, **|
5
5
  xpath = XPath.descendant(:a)
6
6
  xpath = builder(xpath).add_attribute_conditions(href: href) unless href == false
7
7
 
@@ -25,6 +25,7 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
25
25
 
26
26
  xpath = xpath[find_by_attr(:title, title)]
27
27
  xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
28
+ xpath = xpath[find_by_attr(:target, target)] if target
28
29
 
29
30
  xpath
30
31
  end
@@ -3,7 +3,7 @@
3
3
  Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
4
4
  label 'radio button'
5
5
  xpath do |locator, allow_self: nil, **options|
6
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
7
7
  XPath.attr(:type) == 'radio'
8
8
  ]
9
9
  locate_field(xpath, locator, **options)
@@ -203,7 +203,7 @@ module Capybara
203
203
 
204
204
  ##
205
205
  #
206
- # Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
206
+ # Set the default visibility mode that should be used if no visible option is passed when using the selector.
207
207
  # If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
208
208
  #
209
209
  # @param [Symbol] default_visibility Only find elements with the specified visibility:
@@ -260,7 +260,9 @@ module Capybara
260
260
 
261
261
  def parameter_names(block)
262
262
  key_types = %i[key keyreq]
263
- block.parameters.select { |(type, _name)| key_types.include? type }.map { |(_type, name)| name }
263
+ # user filter_map when we drop dupport for 2.6
264
+ # block.parameters.select { |(type, _name)| key_types.include? type }.map { |(_, name)| name }
265
+ block.parameters.filter_map { |(type, name)| name if key_types.include? type }
264
266
  end
265
267
 
266
268
  def expression(type, allowed_filters, &block)
@@ -101,13 +101,10 @@ module Capybara
101
101
  private
102
102
 
103
103
  def options_with_defaults(options)
104
- options = options.dup
105
- [expression_filters, node_filters].each do |filters|
106
- filters.select { |_n, filter| filter.default? }.each do |name, filter|
107
- options[name] = filter.default unless options.key?(name)
108
- end
109
- end
110
- options
104
+ expression_filters
105
+ .chain(node_filters)
106
+ .filter_map { |name, filter| [name, filter.default] if filter.default? }
107
+ .to_h.merge!(options)
111
108
  end
112
109
 
113
110
  def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
@@ -69,11 +69,8 @@ module Capybara
69
69
  suffixes = [[]]
70
70
  strs.reverse_each do |str|
71
71
  if str.is_a? Set
72
- prefixes = str.each_with_object([]) { |s, memo| memo.concat combine(s) }
73
-
74
- result = []
75
- prefixes.product(suffixes) { |pair| result << pair.flatten(1) }
76
- suffixes = result
72
+ prefixes = str.flat_map { |s| combine(s) }
73
+ suffixes = prefixes.product(suffixes).map { |pair| pair.flatten(1) }
77
74
  else
78
75
  suffixes.each { |arr| arr.unshift str }
79
76
  end
@@ -66,7 +66,11 @@ module Capybara
66
66
  end
67
67
  ensure
68
68
  unless locator_valid?(locator)
69
- warn "Locator #{locator.class}:#{locator.inspect} for selector #{name.inspect} must #{locator_description}. This will raise an error in a future version of Capybara."
69
+ Capybara::Helpers.warn(
70
+ "Locator #{locator.class}:#{locator.inspect} for selector #{name.inspect} must #{locator_description}. " \
71
+ 'This will raise an error in a future version of Capybara. ' \
72
+ "Called from: #{Capybara::Helpers.filter_backtrace(caller)}"
73
+ )
70
74
  end
71
75
  end
72
76
 
@@ -14,6 +14,7 @@ require 'capybara/selector/definition'
14
14
  # * :left_of (Element) - Match elements left of the passed element on the page
15
15
  # * :right_of (Element) - Match elements right of the passed element on the page
16
16
  # * :near (Element) - Match elements near (within 50px) the passed element on the page
17
+ # * :focused (Boolean) - Match elements with focus (requires driver support)
17
18
  #
18
19
  # ### Built-in Selectors
19
20
  #
@@ -12,7 +12,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
12
12
  clear_session_storage: nil
13
13
  }.freeze
14
14
  SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout native_displayed].freeze
15
- CAPS_VERSION = Gem::Requirement.new('~> 4.0.0.alpha6')
15
+ CAPS_VERSION = Gem::Requirement.new('> 4.0.0.alpha6', '< 4.8.0')
16
16
 
17
17
  attr_reader :app, :options
18
18
 
@@ -43,7 +43,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
43
43
  Gem::Version.new(Selenium::WebDriver::VERSION)
44
44
  end
45
45
 
46
- unless Gem::Requirement.new('>= 3.5.0').satisfied_by? @selenium_webdriver_version
46
+ unless Gem::Requirement.new('>= 3.142.7').satisfied_by? @selenium_webdriver_version
47
47
  warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
48
48
  end
49
49
 
@@ -148,8 +148,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
148
148
  unwrap_script_result(result)
149
149
  end
150
150
 
151
+ def active_element
152
+ build_node(native_active_element)
153
+ end
154
+
151
155
  def send_keys(*args)
152
- active_element.send_keys(*args)
156
+ # Should this call the specialized nodes rather than native???
157
+ native_active_element.send_keys(*args)
153
158
  end
154
159
 
155
160
  def save_screenshot(path, **_options)
@@ -249,7 +254,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
249
254
  end
250
255
 
251
256
  def open_new_window(kind = :tab)
252
- browser.manage.new_window(kind)
257
+ if browser.switch_to.respond_to?(:new_window)
258
+ handle = current_window_handle
259
+ browser.switch_to.new_window(kind)
260
+ switch_to_window(handle)
261
+ else
262
+ browser.manage.new_window(kind)
263
+ end
253
264
  rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
254
265
  # If not supported by the driver or browser default to using JS
255
266
  browser.execute_script('window.open();')
@@ -293,7 +304,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
293
304
  end
294
305
 
295
306
  def invalid_element_errors
296
- @invalid_element_errors ||= begin
307
+ @invalid_element_errors ||=
297
308
  [
298
309
  ::Selenium::WebDriver::Error::StaleElementReferenceError,
299
310
  ::Selenium::WebDriver::Error::ElementNotInteractableError,
@@ -304,16 +315,18 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
304
315
  ].tap do |errors|
305
316
  unless selenium_4?
306
317
  ::Selenium::WebDriver.logger.suppress_deprecations do
307
- errors.concat [
318
+ errors.push(
308
319
  ::Selenium::WebDriver::Error::UnhandledError,
309
320
  ::Selenium::WebDriver::Error::ElementNotVisibleError,
310
321
  ::Selenium::WebDriver::Error::InvalidElementStateError,
311
322
  ::Selenium::WebDriver::Error::ElementNotSelectableError
312
- ]
323
+ )
313
324
  end
314
325
  end
326
+ if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
327
+ errors.push(::Selenium::WebDriver::Error::DetachedShadowRootError)
328
+ end
315
329
  end
316
- end
317
330
  end
318
331
 
319
332
  def no_such_window_error
@@ -330,6 +343,10 @@ private
330
343
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
331
344
  end
332
345
 
346
+ def native_active_element
347
+ browser.switch_to.active_element
348
+ end
349
+
333
350
  def clear_browser_state
334
351
  delete_all_cookies
335
352
  clear_storage
@@ -459,12 +476,16 @@ private
459
476
  end
460
477
 
461
478
  def unwrap_script_result(arg)
479
+ # TODO: move into the case when we drop support for Selenium < 4.1
480
+ element_types = [Selenium::WebDriver::Element]
481
+ element_types.push(Selenium::WebDriver::ShadowRoot) if defined?(Selenium::WebDriver::ShadowRoot)
482
+
462
483
  case arg
463
484
  when Array
464
485
  arg.map { |arr| unwrap_script_result(arr) }
465
486
  when Hash
466
487
  arg.transform_values! { |value| unwrap_script_result(value) }
467
- when Selenium::WebDriver::Element
488
+ when *element_types
468
489
  build_node(arg)
469
490
  else
470
491
  arg
@@ -475,10 +496,6 @@ private
475
496
  browser
476
497
  end
477
498
 
478
- def active_element
479
- browser.switch_to.active_element
480
- end
481
-
482
499
  def build_node(native_node, initial_cache = {})
483
500
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
484
501
  end
@@ -38,7 +38,7 @@ module Capybara::Selenium::Driver::ChromeDriver
38
38
  return unless @browser
39
39
 
40
40
  switch_to_window(window_handles.first)
41
- window_handles.slice(1..-1).each { |win| close_window(win) }
41
+ window_handles.slice(1..).each { |win| close_window(win) }
42
42
  return super if chromedriver_version < 73
43
43
 
44
44
  timer = Capybara::Helpers.timer(expire_in: 10)
@@ -39,7 +39,7 @@ module Capybara::Selenium::Driver::EdgeDriver
39
39
  return unless @browser
40
40
 
41
41
  switch_to_window(window_handles.first)
42
- window_handles.slice(1..-1).each { |win| close_window(win) }
42
+ window_handles.slice(1..).each { |win| close_window(win) }
43
43
 
44
44
  timer = Capybara::Helpers.timer(expire_in: 10)
45
45
  begin
@@ -103,9 +103,13 @@ private
103
103
  end
104
104
 
105
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']
106
+ if browser.respond_to? :execute_cdp
107
+ browser.execute_cdp(cmd, **params)
108
+ else
109
+ args = { cmd: cmd, params: params }
110
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/ms/cdp/execute", args)
111
+ result['value']
112
+ end
109
113
  end
110
114
 
111
115
  def build_node(native_node, initial_cache = {})
@@ -115,7 +119,7 @@ private
115
119
  def edgedriver_version
116
120
  @edgedriver_version ||= begin
117
121
  caps = browser.capabilities
118
- caps['chrome']&.fetch('chromedriverVersion', nil).to_f
122
+ caps['msedge']&.fetch('msedgedriverVersion', nil).to_f
119
123
  end
120
124
  end
121
125
  end
@@ -10,7 +10,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
10
10
  end
11
11
 
12
12
  def self.w3c?(driver)
13
- (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
13
+ (defined?(Selenium::WebDriver::VERSION) && (Gem::Version.new(Selenium::WebDriver::VERSION) >= Gem::Version.new('4'))) ||
14
14
  driver.browser.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
15
15
  end
16
16
  end
@@ -52,7 +52,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
52
52
  end
53
53
 
54
54
  switch_to_window(window_handles.first)
55
- window_handles.slice(1..-1).each { |win| close_window(win) }
55
+ window_handles.slice(1..).each { |win| close_window(win) }
56
56
  super
57
57
  end
58
58
 
@@ -39,10 +39,8 @@ class Capybara::Selenium::Node
39
39
  input.set_file(args)
40
40
  driver.execute_script DROP_FILE, self, input
41
41
  else
42
- items = args.each_with_object([]) do |arg, arr|
43
- arg.each_with_object(arr) do |(type, data), arr_|
44
- arr_ << { type: type, data: data }
45
- end
42
+ items = args.flat_map do |arg|
43
+ arg.map { |(type, data)| { type: type, data: data } }
46
44
  end
47
45
  driver.execute_script DROP_STRING, items, self
48
46
  end
@@ -168,6 +166,9 @@ class Capybara::Selenium::Node
168
166
  opts[key + 'Key'] = true;
169
167
  }
170
168
 
169
+ var dragEnterEvent = new DragEvent('dragenter', opts);
170
+ target.dispatchEvent(dragEnterEvent);
171
+
171
172
  // fire 2 dragover events to simulate dragging with a direction
172
173
  var entryPoint = pointOnRect(sourceCenter, targetRect)
173
174
  var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);