capybara 3.35.3 → 3.37.1

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