capybara 3.35.3 → 3.37.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +57 -1
  3. data/README.md +5 -1
  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 +3 -12
  9. data/lib/capybara/minitest/spec.rb +2 -2
  10. data/lib/capybara/node/actions.rb +10 -5
  11. data/lib/capybara/node/document.rb +2 -2
  12. data/lib/capybara/node/element.rb +13 -2
  13. data/lib/capybara/node/finders.rb +9 -2
  14. data/lib/capybara/node/simple.rb +5 -1
  15. data/lib/capybara/queries/active_element_query.rb +18 -0
  16. data/lib/capybara/queries/ancestor_query.rb +2 -1
  17. data/lib/capybara/queries/current_path_query.rb +1 -1
  18. data/lib/capybara/queries/selector_query.rb +34 -8
  19. data/lib/capybara/queries/sibling_query.rb +2 -1
  20. data/lib/capybara/rack_test/browser.rb +56 -7
  21. data/lib/capybara/rack_test/driver.rb +4 -4
  22. data/lib/capybara/rack_test/node.rb +10 -7
  23. data/lib/capybara/registration_container.rb +0 -3
  24. data/lib/capybara/registrations/drivers.rb +3 -3
  25. data/lib/capybara/rspec/matcher_proxies.rb +3 -3
  26. data/lib/capybara/rspec/matchers/have_selector.rb +5 -5
  27. data/lib/capybara/rspec/matchers.rb +14 -14
  28. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  29. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  30. data/lib/capybara/selector/css.rb +1 -1
  31. data/lib/capybara/selector/definition/button.rb +9 -4
  32. data/lib/capybara/selector/definition/checkbox.rb +1 -1
  33. data/lib/capybara/selector/definition/file_field.rb +1 -1
  34. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  35. data/lib/capybara/selector/definition/radio_button.rb +1 -1
  36. data/lib/capybara/selector/definition.rb +3 -1
  37. data/lib/capybara/selector/filter_set.rb +4 -6
  38. data/lib/capybara/selector/selector.rb +5 -1
  39. data/lib/capybara/selector.rb +1 -0
  40. data/lib/capybara/selenium/driver.rb +25 -11
  41. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  42. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +1 -1
  43. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +1 -1
  44. data/lib/capybara/selenium/node.rb +22 -8
  45. data/lib/capybara/selenium/nodes/chrome_node.rb +1 -1
  46. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  47. data/lib/capybara/selenium/nodes/firefox_node.rb +1 -1
  48. data/lib/capybara/selenium/nodes/safari_node.rb +2 -2
  49. data/lib/capybara/server/animation_disabler.rb +35 -17
  50. data/lib/capybara/session/config.rb +1 -1
  51. data/lib/capybara/session.rb +20 -23
  52. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  53. data/lib/capybara/spec/session/all_spec.rb +9 -13
  54. data/lib/capybara/spec/session/assert_text_spec.rb +17 -17
  55. data/lib/capybara/spec/session/check_spec.rb +9 -0
  56. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  57. data/lib/capybara/spec/session/find_spec.rb +6 -0
  58. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  59. data/lib/capybara/spec/session/has_button_spec.rb +24 -0
  60. data/lib/capybara/spec/session/has_current_path_spec.rb +2 -2
  61. data/lib/capybara/spec/session/has_field_spec.rb +25 -1
  62. data/lib/capybara/spec/session/has_link_spec.rb +30 -0
  63. data/lib/capybara/spec/session/has_select_spec.rb +4 -4
  64. data/lib/capybara/spec/session/has_selector_spec.rb +15 -0
  65. data/lib/capybara/spec/session/has_text_spec.rb +2 -6
  66. data/lib/capybara/spec/session/node_spec.rb +43 -1
  67. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  68. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  69. data/lib/capybara/spec/session/window/window_spec.rb +1 -1
  70. data/lib/capybara/spec/spec_helper.rb +4 -3
  71. data/lib/capybara/spec/test_app.rb +66 -8
  72. data/lib/capybara/spec/views/animated.erb +1 -1
  73. data/lib/capybara/spec/views/form.erb +11 -3
  74. data/lib/capybara/spec/views/frame_child.erb +1 -1
  75. data/lib/capybara/spec/views/frame_one.erb +1 -1
  76. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  77. data/lib/capybara/spec/views/frame_two.erb +1 -1
  78. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  79. data/lib/capybara/spec/views/layout.erb +10 -0
  80. data/lib/capybara/spec/views/obscured.erb +1 -1
  81. data/lib/capybara/spec/views/offset.erb +2 -1
  82. data/lib/capybara/spec/views/path.erb +2 -2
  83. data/lib/capybara/spec/views/popup_one.erb +1 -1
  84. data/lib/capybara/spec/views/popup_two.erb +1 -1
  85. data/lib/capybara/spec/views/react.erb +2 -2
  86. data/lib/capybara/spec/views/scroll.erb +2 -1
  87. data/lib/capybara/spec/views/spatial.erb +1 -1
  88. data/lib/capybara/spec/views/with_animation.erb +2 -3
  89. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  90. data/lib/capybara/spec/views/with_dragula.erb +2 -2
  91. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  92. data/lib/capybara/spec/views/with_hover.erb +2 -2
  93. data/lib/capybara/spec/views/with_html.erb +1 -1
  94. data/lib/capybara/spec/views/with_jquery_animation.erb +1 -1
  95. data/lib/capybara/spec/views/with_js.erb +2 -3
  96. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  97. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  98. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  99. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  100. data/lib/capybara/spec/views/with_sortable_js.erb +2 -2
  101. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  102. data/lib/capybara/spec/views/with_windows.erb +1 -1
  103. data/lib/capybara/spec/views/within_frames.erb +1 -1
  104. data/lib/capybara/version.rb +1 -1
  105. data/lib/capybara/window.rb +1 -1
  106. data/lib/capybara.rb +19 -22
  107. data/spec/basic_node_spec.rb +16 -3
  108. data/spec/dsl_spec.rb +3 -3
  109. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  110. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  111. data/spec/rack_test_spec.rb +20 -10
  112. data/spec/result_spec.rb +32 -35
  113. data/spec/rspec/features_spec.rb +3 -3
  114. data/spec/rspec/scenarios_spec.rb +1 -1
  115. data/spec/rspec/shared_spec_matchers.rb +2 -2
  116. data/spec/sauce_spec_chrome.rb +3 -3
  117. data/spec/selector_spec.rb +2 -2
  118. data/spec/selenium_spec_chrome.rb +9 -10
  119. data/spec/selenium_spec_chrome_remote.rb +9 -8
  120. data/spec/selenium_spec_firefox.rb +8 -3
  121. data/spec/selenium_spec_firefox_remote.rb +2 -2
  122. data/spec/selenium_spec_ie.rb +3 -6
  123. data/spec/selenium_spec_safari.rb +31 -19
  124. data/spec/server_spec.rb +5 -5
  125. data/spec/shared_selenium_node.rb +0 -4
  126. data/spec/shared_selenium_session.rb +20 -10
  127. data/spec/spec_helper.rb +1 -1
  128. metadata +37 -14
  129. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -20,6 +20,8 @@ class Capybara::RackTest::Browser
20
20
  end
21
21
 
22
22
  def visit(path, **attributes)
23
+ @new_visit_request = true
24
+ reset_cache!
23
25
  reset_host!
24
26
  process_and_follow_redirects(:get, path, attributes)
25
27
  end
@@ -33,19 +35,18 @@ class Capybara::RackTest::Browser
33
35
  path = request_path if path.nil? || path.empty?
34
36
  uri = build_uri(path)
35
37
  uri.query = '' if method.to_s.casecmp('get').zero?
36
- process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => current_url)
38
+ process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => referer_url)
37
39
  end
38
40
 
39
41
  def follow(method, path, **attributes)
40
42
  return if fragment_or_script?(path)
41
43
 
42
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
44
+ process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => referer_url)
43
45
  end
44
46
 
45
47
  def process_and_follow_redirects(method, path, attributes = {}, env = {})
46
48
  @current_fragment = build_uri(path).fragment
47
49
  process(method, path, attributes, env)
48
-
49
50
  return unless driver.follow_redirects?
50
51
 
51
52
  driver.redirect_limit.times do
@@ -69,18 +70,23 @@ class Capybara::RackTest::Browser
69
70
  @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
70
71
  @current_fragment = new_uri.fragment || @current_fragment
71
72
  reset_cache!
73
+ @new_visit_request = false
72
74
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
73
75
  end
74
76
 
75
77
  def build_uri(path)
76
- URI.parse(path).tap do |uri|
77
- uri.path = request_path if path.empty? || path.start_with?('?')
78
- uri.path = '/' if uri.path.empty?
79
- uri.path = request_path.sub(%r{/[^/]*$}, '/') + uri.path unless uri.path.start_with?('/')
78
+ uri = URI.parse(path)
79
+ base_uri = base_relative_uri_for(uri)
80
80
 
81
+ uri.path = base_uri.path + uri.path unless uri.absolute? || uri.path.start_with?('/')
82
+
83
+ if base_uri.absolute?
84
+ base_uri.merge(uri)
85
+ else
81
86
  uri.scheme ||= @current_scheme
82
87
  uri.host ||= @current_host
83
88
  uri.port ||= @current_port unless uri.default_port == @current_port
89
+ uri
84
90
  end
85
91
  end
86
92
 
@@ -123,8 +129,39 @@ class Capybara::RackTest::Browser
123
129
  dom.title
124
130
  end
125
131
 
132
+ def last_request
133
+ raise Rack::Test::Error if @new_visit_request
134
+
135
+ super
136
+ end
137
+
138
+ def last_response
139
+ raise Rack::Test::Error if @new_visit_request
140
+
141
+ super
142
+ end
143
+
126
144
  protected
127
145
 
146
+ def base_href
147
+ find(:css, 'head > base').first&.[](:href).to_s
148
+ end
149
+
150
+ def base_relative_uri_for(uri)
151
+ base_uri = URI.parse(base_href)
152
+ current_uri = URI.parse(safe_last_request&.url.to_s).tap do |c|
153
+ c.path.sub!(%r{/[^/]*$}, '/') unless uri.path.empty?
154
+ c.path = '/' if c.path.empty?
155
+ end
156
+
157
+ if [current_uri, base_uri].any?(&:absolute?)
158
+ current_uri.merge(base_uri)
159
+ else
160
+ base_uri.path = current_uri.path if base_uri.path.empty?
161
+ base_uri
162
+ end
163
+ end
164
+
128
165
  def build_rack_mock_session
129
166
  reset_host! unless current_host
130
167
  Rack::MockSession.new(app, current_host)
@@ -136,9 +173,21 @@ protected
136
173
  '/'
137
174
  end
138
175
 
176
+ def safe_last_request
177
+ last_request
178
+ rescue Rack::Test::Error
179
+ nil
180
+ end
181
+
139
182
  private
140
183
 
141
184
  def fragment_or_script?(path)
142
185
  path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
143
186
  end
187
+
188
+ def referer_url
189
+ build_uri(last_request.url).to_s
190
+ rescue Rack::Test::Error
191
+ ''
192
+ end
144
193
  end
@@ -98,10 +98,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
98
98
  @browser = nil
99
99
  end
100
100
 
101
- def get(*args, &block); browser.get(*args, &block); end
102
- def post(*args, &block); browser.post(*args, &block); end
103
- def put(*args, &block); browser.put(*args, &block); end
104
- def delete(*args, &block); browser.delete(*args, &block); end
101
+ def get(...); browser.get(...); end
102
+ def post(...); browser.post(...); end
103
+ def put(...); browser.put(...); end
104
+ def delete(...); browser.delete(...); end
105
105
  def header(key, value); browser.header(key, value); end
106
106
 
107
107
  def invalid_element_errors
@@ -108,6 +108,13 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
108
108
  end
109
109
  end
110
110
 
111
+ def readonly?
112
+ # readonly attribute not valid on these input types
113
+ return false if input_field? && %w[hidden range color checkbox radio file submit image reset button].include?(type)
114
+
115
+ super
116
+ end
117
+
111
118
  def path
112
119
  native.path
113
120
  end
@@ -139,10 +146,6 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
139
146
  end
140
147
  end
141
148
 
142
- def ==(other)
143
- native == other.native
144
- end
145
-
146
149
  protected
147
150
 
148
151
  # @api private
@@ -159,7 +162,7 @@ protected
159
162
  end.join || ''
160
163
  text = "\n#{text}\n" if BLOCK_ELEMENTS.include?(tag_name)
161
164
  text
162
- else
165
+ else # rubocop:disable Lint/DuplicateBranch
163
166
  ''
164
167
  end
165
168
  end
@@ -213,7 +216,7 @@ private
213
216
  min, max, step = (native['min'] || 0).to_f, (native['max'] || 100).to_f, (native['step'] || 1).to_f
214
217
  value = value.to_f
215
218
  value = value.clamp(min, max)
216
- value = ((value - min) / step).round * step + min
219
+ value = (((value - min) / step).round * step) + min
217
220
  native['value'] = value.clamp(min, max)
218
221
  end
219
222
 
@@ -241,7 +244,7 @@ private
241
244
  end
242
245
 
243
246
  def follow_link
244
- method = self['data-method'] if driver.options[:respect_data_method]
247
+ method = self['data-method'] || self['data-turbo-method'] if driver.options[:respect_data_method]
245
248
  method ||= :get
246
249
  driver.follow(method, self[:href].to_s)
247
250
  end
@@ -19,9 +19,6 @@ module Capybara
19
19
  def method_missing(method_name, *args, **options, &block)
20
20
  if @registered.respond_to?(method_name)
21
21
  Capybara::Helpers.warn "DEPRECATED: Calling '#{method_name}' on the drivers/servers container is deprecated without replacement"
22
- # RUBY 2.6 will send an empty hash rather than nothing with **options so fix that
23
- return @registered.public_send(method_name, *args, &block) if options.empty?
24
-
25
22
  return @registered.public_send(method_name, *args, **options, &block)
26
23
  end
27
24
  super
@@ -14,7 +14,7 @@ Capybara.register_driver :selenium_headless do |app|
14
14
  browser_options = ::Selenium::WebDriver::Firefox::Options.new.tap do |opts|
15
15
  opts.add_argument '-headless'
16
16
  end
17
- Capybara::Selenium::Driver.new(app, **Hash[:browser => :firefox, options_key => browser_options])
17
+ Capybara::Selenium::Driver.new(app, **{ :browser => :firefox, options_key => browser_options })
18
18
  end
19
19
 
20
20
  Capybara.register_driver :selenium_chrome do |app|
@@ -25,7 +25,7 @@ Capybara.register_driver :selenium_chrome do |app|
25
25
  opts.add_argument('--disable-site-isolation-trials')
26
26
  end
27
27
 
28
- Capybara::Selenium::Driver.new(app, **Hash[:browser => :chrome, options_key => browser_options])
28
+ Capybara::Selenium::Driver.new(app, **{ :browser => :chrome, options_key => browser_options })
29
29
  end
30
30
 
31
31
  Capybara.register_driver :selenium_chrome_headless do |app|
@@ -38,5 +38,5 @@ Capybara.register_driver :selenium_chrome_headless do |app|
38
38
  opts.add_argument('--disable-site-isolation-trials')
39
39
  end
40
40
 
41
- Capybara::Selenium::Driver.new(app, **Hash[:browser => :chrome, options_key => browser_options])
41
+ Capybara::Selenium::Driver.new(app, **{ :browser => :chrome, options_key => browser_options })
42
42
  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)
@@ -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,7 +70,7 @@ 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
@@ -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)
@@ -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)
@@ -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,11 @@ 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
104
+ expression_filters.chain(node_filters)
105
+ .select { |_n, filter| filter.default? }
106
+ .each_with_object(options.dup) do |(name, filter), opts|
107
+ opts[name] = filter.default unless opts.key?(name)
109
108
  end
110
- options
111
109
  end
112
110
 
113
111
  def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
@@ -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')
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,
@@ -313,7 +324,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
313
324
  end
314
325
  end
315
326
  end
316
- end
317
327
  end
318
328
 
319
329
  def no_such_window_error
@@ -330,6 +340,10 @@ private
330
340
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
331
341
  end
332
342
 
343
+ def native_active_element
344
+ browser.switch_to.active_element
345
+ end
346
+
333
347
  def clear_browser_state
334
348
  delete_all_cookies
335
349
  clear_storage
@@ -459,12 +473,16 @@ private
459
473
  end
460
474
 
461
475
  def unwrap_script_result(arg)
476
+ # TODO: move into the case when we drop support for Selenium < 4.1
477
+ element_types = [Selenium::WebDriver::Element]
478
+ element_types.push(Selenium::WebDriver::ShadowRoot) if defined?(Selenium::WebDriver::ShadowRoot)
479
+
462
480
  case arg
463
481
  when Array
464
482
  arg.map { |arr| unwrap_script_result(arr) }
465
483
  when Hash
466
484
  arg.transform_values! { |value| unwrap_script_result(value) }
467
- when Selenium::WebDriver::Element
485
+ when *element_types
468
486
  build_node(arg)
469
487
  else
470
488
  arg
@@ -475,10 +493,6 @@ private
475
493
  browser
476
494
  end
477
495
 
478
- def active_element
479
- browser.switch_to.active_element
480
- end
481
-
482
496
  def build_node(native_node, initial_cache = {})
483
497
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
484
498
  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
@@ -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