capybara 3.35.2 → 3.37.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +59 -4
  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 +2 -11
  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 +2 -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 +41 -6
  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.rb +1 -0
  39. data/lib/capybara/selenium/driver.rb +25 -11
  40. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  41. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +1 -1
  42. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +1 -1
  43. data/lib/capybara/selenium/node.rb +22 -8
  44. data/lib/capybara/selenium/nodes/chrome_node.rb +1 -1
  45. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  46. data/lib/capybara/selenium/nodes/firefox_node.rb +1 -1
  47. data/lib/capybara/selenium/nodes/safari_node.rb +2 -2
  48. data/lib/capybara/server/animation_disabler.rb +35 -17
  49. data/lib/capybara/session/config.rb +1 -1
  50. data/lib/capybara/session.rb +20 -23
  51. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  52. data/lib/capybara/spec/session/all_spec.rb +9 -13
  53. data/lib/capybara/spec/session/assert_text_spec.rb +17 -17
  54. data/lib/capybara/spec/session/check_spec.rb +9 -0
  55. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  56. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  57. data/lib/capybara/spec/session/has_button_spec.rb +24 -0
  58. data/lib/capybara/spec/session/has_current_path_spec.rb +2 -2
  59. data/lib/capybara/spec/session/has_field_spec.rb +25 -1
  60. data/lib/capybara/spec/session/has_link_spec.rb +24 -0
  61. data/lib/capybara/spec/session/has_select_spec.rb +4 -4
  62. data/lib/capybara/spec/session/has_selector_spec.rb +15 -0
  63. data/lib/capybara/spec/session/has_text_spec.rb +2 -6
  64. data/lib/capybara/spec/session/node_spec.rb +43 -1
  65. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  66. data/lib/capybara/spec/session/visit_spec.rb +14 -0
  67. data/lib/capybara/spec/session/window/window_spec.rb +1 -1
  68. data/lib/capybara/spec/spec_helper.rb +4 -3
  69. data/lib/capybara/spec/test_app.rb +50 -8
  70. data/lib/capybara/spec/views/animated.erb +1 -1
  71. data/lib/capybara/spec/views/form.erb +11 -3
  72. data/lib/capybara/spec/views/frame_child.erb +1 -1
  73. data/lib/capybara/spec/views/frame_one.erb +1 -1
  74. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  75. data/lib/capybara/spec/views/frame_two.erb +1 -1
  76. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  77. data/lib/capybara/spec/views/layout.erb +10 -0
  78. data/lib/capybara/spec/views/obscured.erb +1 -1
  79. data/lib/capybara/spec/views/offset.erb +2 -1
  80. data/lib/capybara/spec/views/path.erb +2 -2
  81. data/lib/capybara/spec/views/popup_one.erb +1 -1
  82. data/lib/capybara/spec/views/popup_two.erb +1 -1
  83. data/lib/capybara/spec/views/react.erb +2 -2
  84. data/lib/capybara/spec/views/scroll.erb +2 -1
  85. data/lib/capybara/spec/views/spatial.erb +1 -1
  86. data/lib/capybara/spec/views/with_animation.erb +2 -3
  87. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  88. data/lib/capybara/spec/views/with_dragula.erb +2 -2
  89. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  90. data/lib/capybara/spec/views/with_hover.erb +2 -2
  91. data/lib/capybara/spec/views/with_html.erb +1 -1
  92. data/lib/capybara/spec/views/with_jquery_animation.erb +1 -1
  93. data/lib/capybara/spec/views/with_js.erb +2 -3
  94. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  95. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  96. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  97. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  98. data/lib/capybara/spec/views/with_sortable_js.erb +2 -2
  99. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  100. data/lib/capybara/spec/views/with_windows.erb +1 -1
  101. data/lib/capybara/spec/views/within_frames.erb +1 -1
  102. data/lib/capybara/version.rb +1 -1
  103. data/lib/capybara/window.rb +1 -1
  104. data/lib/capybara.rb +19 -22
  105. data/spec/basic_node_spec.rb +16 -3
  106. data/spec/dsl_spec.rb +3 -3
  107. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  108. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  109. data/spec/rack_test_spec.rb +20 -10
  110. data/spec/result_spec.rb +32 -35
  111. data/spec/rspec/features_spec.rb +3 -3
  112. data/spec/rspec/scenarios_spec.rb +1 -1
  113. data/spec/rspec/shared_spec_matchers.rb +2 -2
  114. data/spec/sauce_spec_chrome.rb +3 -3
  115. data/spec/selector_spec.rb +1 -1
  116. data/spec/selenium_spec_chrome.rb +9 -10
  117. data/spec/selenium_spec_chrome_remote.rb +9 -8
  118. data/spec/selenium_spec_firefox.rb +8 -3
  119. data/spec/selenium_spec_firefox_remote.rb +2 -2
  120. data/spec/selenium_spec_ie.rb +3 -6
  121. data/spec/selenium_spec_safari.rb +31 -19
  122. data/spec/server_spec.rb +5 -5
  123. data/spec/shared_selenium_node.rb +0 -4
  124. data/spec/shared_selenium_session.rb +20 -10
  125. data/spec/spec_helper.rb +1 -1
  126. metadata +37 -14
  127. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -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)
@@ -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
 
@@ -14,7 +14,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
14
14
  end
15
15
 
16
16
  def all_text
17
- text = driver.evaluate_script('arguments[0].textContent', self)
17
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
18
18
  text.gsub(/[\u200b\u200e\u200f]/, '')
19
19
  .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
20
20
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
@@ -53,6 +53,11 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
53
53
  # :none => append the new value to the existing value <br/>
54
54
  # :backspace => send backspace keystrokes to clear the field <br/>
55
55
  # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
56
+ # @option options [Boolean] :rapid (nil) Whether setting text inputs should use a faster &quot;rapid&quot; mode<br/>
57
+ # nil => Text inputs with length greater than 30 characters will be set using a faster driver script mode<br/>
58
+ # true => Rapid mode will be used regardless of input length<br/>
59
+ # false => Sends keys via conventional mode. This may be required to avoid losing key-presses if you have certain
60
+ # Javascript interactions on form inputs<br/>
56
61
  def set(value, **options)
57
62
  if value.is_a?(Array) && !multiple?
58
63
  raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
@@ -199,10 +204,6 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
199
204
  native.attribute('isContentEditable') == 'true'
200
205
  end
201
206
 
202
- def ==(other)
203
- native == other.native
204
- end
205
-
206
207
  def path
207
208
  driver.evaluate_script GET_XPATH_SCRIPT, self
208
209
  end
@@ -218,6 +219,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
218
219
  native.rect
219
220
  end
220
221
 
222
+ def shadow_root
223
+ raise_error 'You must be using Selenium 4.1+ for shadow_root support' unless native.respond_to? :shadow_root
224
+
225
+ root = native.shadow_root
226
+ root && build_node(native.shadow_root)
227
+ end
228
+
221
229
  protected
222
230
 
223
231
  def scroll_if_needed
@@ -274,7 +282,7 @@ private
274
282
  elsif clear == :backspace
275
283
  # Clear field by sending the correct number of backspace keys.
276
284
  backspaces = [:backspace] * self.value.to_s.length
277
- send_keys(*([:end] + backspaces + [value]))
285
+ send_keys(:end, *backspaces, value)
278
286
  elsif clear.is_a? Array
279
287
  send_keys(*clear, value)
280
288
  else
@@ -282,7 +290,7 @@ private
282
290
  if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
283
291
  send_keys(value[0..3])
284
292
  driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
285
- send_keys(value[-3..-1])
293
+ send_keys(value[-3..])
286
294
  else
287
295
  send_keys(value)
288
296
  end
@@ -298,7 +306,7 @@ private
298
306
 
299
307
  scroll_if_needed do
300
308
  action_with_modifiers(click_options) do |action|
301
- if block_given?
309
+ if block
302
310
  yield action
303
311
  else
304
312
  click_options.coords? ? action.click : action.click(native)
@@ -488,6 +496,12 @@ private
488
496
  JS
489
497
  end
490
498
 
499
+ def native_id
500
+ # Selenium 3 -> 4 changed the return of ref
501
+ type_or_id, id = native.ref
502
+ id || type_or_id
503
+ end
504
+
491
505
  GET_XPATH_SCRIPT = <<~'JS'
492
506
  (function(el, xml){
493
507
  var xpath = '';
@@ -65,7 +65,7 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
65
65
  return super unless native_displayed?
66
66
 
67
67
  begin
68
- bridge.send(:execute, :is_element_displayed, id: native.ref)
68
+ bridge.send(:execute, :is_element_displayed, id: native_id)
69
69
  rescue Selenium::WebDriver::Error::UnknownCommandError
70
70
  # If the is_element_displayed command is unknown, no point in trying again
71
71
  driver.options[:native_displayed] = false
@@ -69,7 +69,7 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
69
69
  return super unless chrome_edge? && native_displayed?
70
70
 
71
71
  begin
72
- bridge.send(:execute, :is_element_displayed, id: native.ref)
72
+ bridge.send(:execute, :is_element_displayed, id: native_id)
73
73
  rescue Selenium::WebDriver::Error::UnknownCommandError
74
74
  # If the is_element_displayed command is unknown, no point in trying again
75
75
  driver.options[:native_displayed] = false
@@ -76,7 +76,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
76
76
  return super unless native_displayed?
77
77
 
78
78
  begin
79
- bridge.send(:execute, :is_element_displayed, id: native.ref)
79
+ bridge.send(:execute, :is_element_displayed, id: native_id)
80
80
  rescue Selenium::WebDriver::Error::UnknownCommandError
81
81
  # If the is_element_displayed command is unknown, no point in trying again
82
82
  driver.options[:native_displayed] = false
@@ -14,7 +14,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
14
14
  warn 'You are attempting to click a table row which has issues in safaridriver - '\
15
15
  'Your test should probably be clicking on a table cell like a user would. '\
16
16
  'Clicking the first cell in the row instead.'
17
- return find_css('th:first-child,td:first-child')[0].click(keys, options)
17
+ return find_css('th:first-child,td:first-child')[0].click(keys, **options)
18
18
  end
19
19
  raise
20
20
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
@@ -74,7 +74,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
74
74
  if clear == :backspace
75
75
  # Clear field by sending the correct number of backspace keys.
76
76
  backspaces = [:backspace] * self.value.to_s.length
77
- send_keys(*([[:control, 'e']] + backspaces + [value]))
77
+ send_keys([:control, 'e'], *backspaces, value)
78
78
  else
79
79
  super.tap do
80
80
  # React doesn't see the safaridriver element clear