capybara 3.32.0 → 3.35.0

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +99 -15
  3. data/README.md +9 -4
  4. data/lib/capybara.rb +18 -8
  5. data/lib/capybara/config.rb +4 -6
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +4 -0
  8. data/lib/capybara/helpers.rb +25 -1
  9. data/lib/capybara/minitest.rb +2 -3
  10. data/lib/capybara/minitest/spec.rb +14 -11
  11. data/lib/capybara/node/actions.rb +16 -21
  12. data/lib/capybara/node/base.rb +6 -6
  13. data/lib/capybara/node/element.rb +1 -5
  14. data/lib/capybara/node/finders.rb +7 -6
  15. data/lib/capybara/node/matchers.rb +12 -12
  16. data/lib/capybara/node/simple.rb +5 -1
  17. data/lib/capybara/queries/ancestor_query.rb +1 -1
  18. data/lib/capybara/queries/current_path_query.rb +14 -4
  19. data/lib/capybara/queries/selector_query.rb +40 -18
  20. data/lib/capybara/queries/sibling_query.rb +1 -1
  21. data/lib/capybara/queries/style_query.rb +1 -1
  22. data/lib/capybara/queries/text_query.rb +7 -1
  23. data/lib/capybara/rack_test/browser.rb +7 -3
  24. data/lib/capybara/rack_test/driver.rb +1 -0
  25. data/lib/capybara/rack_test/form.rb +1 -1
  26. data/lib/capybara/rack_test/node.rb +1 -1
  27. data/lib/capybara/registration_container.rb +44 -0
  28. data/lib/capybara/registrations/drivers.rb +18 -12
  29. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  30. data/lib/capybara/registrations/servers.rb +3 -2
  31. data/lib/capybara/result.rb +10 -11
  32. data/lib/capybara/rspec.rb +2 -0
  33. data/lib/capybara/rspec/matcher_proxies.rb +1 -1
  34. data/lib/capybara/rspec/matchers.rb +7 -6
  35. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  36. data/lib/capybara/rspec/matchers/have_text.rb +1 -1
  37. data/lib/capybara/rspec/matchers/match_style.rb +5 -0
  38. data/lib/capybara/selector.rb +12 -3
  39. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  40. data/lib/capybara/selector/builders/xpath_builder.rb +3 -1
  41. data/lib/capybara/selector/definition.rb +11 -9
  42. data/lib/capybara/selector/definition/button.rb +26 -14
  43. data/lib/capybara/selector/definition/css.rb +1 -1
  44. data/lib/capybara/selector/definition/datalist_input.rb +1 -1
  45. data/lib/capybara/selector/definition/element.rb +2 -1
  46. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  47. data/lib/capybara/selector/definition/label.rb +1 -1
  48. data/lib/capybara/selector/definition/link.rb +8 -0
  49. data/lib/capybara/selector/definition/select.rb +1 -1
  50. data/lib/capybara/selector/definition/table.rb +1 -1
  51. data/lib/capybara/selector/definition/table_row.rb +2 -2
  52. data/lib/capybara/selector/filter_set.rb +2 -2
  53. data/lib/capybara/selector/selector.rb +9 -1
  54. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  55. data/lib/capybara/selenium/driver.rb +51 -7
  56. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +9 -11
  57. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +9 -11
  58. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +3 -3
  59. data/lib/capybara/selenium/extensions/find.rb +4 -4
  60. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  61. data/lib/capybara/selenium/logger_suppressor.rb +1 -1
  62. data/lib/capybara/selenium/node.rb +23 -6
  63. data/lib/capybara/selenium/nodes/chrome_node.rb +23 -5
  64. data/lib/capybara/selenium/nodes/firefox_node.rb +7 -2
  65. data/lib/capybara/selenium/nodes/safari_node.rb +1 -1
  66. data/lib/capybara/selenium/patches/action_pauser.rb +4 -1
  67. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  68. data/lib/capybara/selenium/patches/logs.rb +7 -9
  69. data/lib/capybara/server/animation_disabler.rb +8 -3
  70. data/lib/capybara/server/middleware.rb +4 -2
  71. data/lib/capybara/session.rb +23 -14
  72. data/lib/capybara/session/config.rb +3 -1
  73. data/lib/capybara/session/matchers.rb +11 -11
  74. data/lib/capybara/spec/public/test.js +13 -1
  75. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  76. data/lib/capybara/spec/session/check_spec.rb +6 -0
  77. data/lib/capybara/spec/session/click_button_spec.rb +11 -0
  78. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  79. data/lib/capybara/spec/session/has_button_spec.rb +51 -0
  80. data/lib/capybara/spec/session/has_css_spec.rb +2 -1
  81. data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
  82. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  83. data/lib/capybara/spec/session/has_select_spec.rb +4 -4
  84. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  85. data/lib/capybara/spec/session/has_text_spec.rb +0 -11
  86. data/lib/capybara/spec/session/html_spec.rb +1 -1
  87. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  88. data/lib/capybara/spec/session/node_spec.rb +29 -9
  89. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  90. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  91. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  92. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  93. data/lib/capybara/spec/session/window/window_spec.rb +1 -1
  94. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  95. data/lib/capybara/spec/spec_helper.rb +12 -12
  96. data/lib/capybara/spec/test_app.rb +23 -21
  97. data/lib/capybara/spec/views/form.erb +28 -1
  98. data/lib/capybara/spec/views/with_animation.erb +8 -0
  99. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  100. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  101. data/lib/capybara/spec/views/with_js.erb +3 -0
  102. data/lib/capybara/spec/views/with_sortable_js.erb +1 -1
  103. data/lib/capybara/version.rb +1 -1
  104. data/lib/capybara/window.rb +3 -7
  105. data/spec/basic_node_spec.rb +9 -8
  106. data/spec/capybara_spec.rb +1 -1
  107. data/spec/dsl_spec.rb +14 -1
  108. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  109. data/spec/minitest_spec.rb +3 -2
  110. data/spec/rack_test_spec.rb +16 -5
  111. data/spec/result_spec.rb +1 -17
  112. data/spec/rspec/features_spec.rb +3 -1
  113. data/spec/rspec/scenarios_spec.rb +4 -0
  114. data/spec/rspec/shared_spec_matchers.rb +63 -51
  115. data/spec/rspec_spec.rb +4 -0
  116. data/spec/selector_spec.rb +17 -2
  117. data/spec/selenium_spec_chrome.rb +39 -20
  118. data/spec/selenium_spec_chrome_remote.rb +5 -1
  119. data/spec/selenium_spec_firefox.rb +15 -13
  120. data/spec/server_spec.rb +60 -49
  121. data/spec/shared_selenium_node.rb +10 -0
  122. data/spec/shared_selenium_session.rb +98 -7
  123. data/spec/spec_helper.rb +1 -1
  124. metadata +50 -15
  125. data/lib/capybara/spec/session/source_spec.rb +0 -0
@@ -15,6 +15,8 @@ module Capybara
15
15
  def add_attribute_conditions(**conditions)
16
16
  @expression = conditions.inject(expression) do |xp, (name, value)|
17
17
  conditions = name == :class ? class_conditions(value) : attribute_conditions(name => value)
18
+ return xp if conditions.nil?
19
+
18
20
  if xp.is_a? XPath::Expression
19
21
  xp[conditions]
20
22
  else
@@ -47,7 +49,7 @@ module Capybara
47
49
  when XPath::Expression, Regexp
48
50
  attribute_conditions(class: classes)
49
51
  else
50
- Array(classes).map do |klass|
52
+ Array(classes).reject { |c| c.is_a? Regexp }.map do |klass|
51
53
  if klass.match?(/^!(?!!!)/)
52
54
  !XPath.attr(:class).contains_word(klass.slice(1..-1))
53
55
  else
@@ -10,11 +10,12 @@ module Capybara
10
10
  class Selector
11
11
  class Definition
12
12
  attr_reader :name, :expressions
13
+
13
14
  extend Forwardable
14
15
 
15
16
  def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
16
17
  @name = name
17
- @filter_set = Capybara::Selector::FilterSet.add(name) {}
18
+ @filter_set = Capybara::Selector::FilterSet.add(name)
18
19
  @match = nil
19
20
  @label = nil
20
21
  @failure_message = nil
@@ -82,7 +83,7 @@ module Capybara
82
83
  # Automatic selector detection
83
84
  #
84
85
  # @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
85
- # @yieldparam [String], locator The locator string used to determin if it matches the selector
86
+ # @yieldparam [String], locator The locator string used to determine if it matches the selector
86
87
  # @yieldreturn [Boolean] Whether this selector matches the locator string
87
88
  # @return [#call] The block that will be used to detect selector match
88
89
  #
@@ -177,7 +178,7 @@ module Capybara
177
178
  def_delegator :@filter_set, :describe
178
179
 
179
180
  def describe_expression_filters(&block)
180
- if block_given?
181
+ if block
181
182
  describe(:expression_filters, &block)
182
183
  else
183
184
  describe(:expression_filters) do |**options|
@@ -189,7 +190,7 @@ module Capybara
189
190
  def describe_all_expression_filters(**opts)
190
191
  expression_filters.map do |ef_name, ef|
191
192
  if ef.matcher?
192
- handled_custom_keys(ef, opts.keys).map { |key| " with #{ef_name}[#{key} => #{opts[key]}]" }.join
193
+ handled_custom_options(ef, opts).map { |option, value| " with #{ef_name}[#{option} => #{value}]" }.join
193
194
  elsif opts.key?(ef_name)
194
195
  " with #{ef_name} #{opts[ef_name]}"
195
196
  end
@@ -214,7 +215,7 @@ module Capybara
214
215
  end
215
216
 
216
217
  def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
217
- vis = if @default_visibility&.respond_to?(:call)
218
+ vis = if @default_visibility.respond_to?(:call)
218
219
  @default_visibility.call(options)
219
220
  else
220
221
  @default_visibility
@@ -251,14 +252,15 @@ module Capybara
251
252
 
252
253
  private
253
254
 
254
- def handled_custom_keys(filter, keys)
255
- keys.select do |key|
256
- filter.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
255
+ def handled_custom_options(filter, options)
256
+ options.select do |option, _|
257
+ filter.handles_option?(option) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(option)
257
258
  end
258
259
  end
259
260
 
260
261
  def parameter_names(block)
261
- block.parameters.select { |(type, _name)| %i[key keyreq].include? type }.map { |(_type, name)| name }
262
+ key_types = %i[key keyreq]
263
+ block.parameters.select { |(type, _name)| key_types.include? type }.map { |(_type, name)| name }
262
264
  end
263
265
 
264
266
  def expression(type, allowed_filters, &block)
@@ -4,30 +4,30 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
4
4
  xpath(:value, :title, :type, :name) do |locator, **options|
5
5
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
6
6
  btn_xpath = XPath.descendant(:button)
7
+ aria_btn_xpath = XPath.descendant[XPath.attr(:role).equals('button')]
7
8
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
8
9
 
9
10
  unless locator.nil?
10
11
  locator = locator.to_s
11
- locator_matchers = XPath.attr(:id).equals(locator) |
12
- XPath.attr(:name).equals(locator) |
13
- XPath.attr(:value).is(locator) |
14
- XPath.attr(:title).is(locator)
15
- locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
16
- locator_matchers |= XPath.attr(test_id) == locator if test_id
12
+ locator_matchers = combine_locators(locator, config: self)
13
+ btn_matchers = locator_matchers |
14
+ XPath.string.n.is(locator) |
15
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
17
16
 
18
- input_btn_xpath = input_btn_xpath[locator_matchers]
19
-
20
- btn_xpath = btn_xpath[locator_matchers |
21
- XPath.string.n.is(locator) |
22
- XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
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)
19
+ aria_btn_xpath = aria_btn_xpath[btn_matchers]
23
20
 
24
21
  alt_matches = XPath.attr(:alt).is(locator)
25
22
  alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
26
- image_btn_xpath = image_btn_xpath[alt_matches]
23
+ image_btn_xpath = image_btn_xpath[alt_matches] + locate_label(locator).descendant(image_btn_xpath)
27
24
  end
28
25
 
29
- %i[value title type].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
30
- memo[find_by_attr(ef, options[ef])]
26
+ btn_xpaths = [input_btn_xpath, btn_xpath, image_btn_xpath]
27
+ btn_xpaths << aria_btn_xpath if enable_aria_role
28
+
29
+ %i[value title type].inject(btn_xpaths.inject(&:union)) do |memo, ef|
30
+ memo.where(find_by_attr(ef, options[ef]))
31
31
  end
32
32
  end
33
33
 
@@ -48,4 +48,16 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
48
48
  describe_node_filters do |disabled: nil, **|
49
49
  ' that is disabled' if disabled == true
50
50
  end
51
+
52
+ def combine_locators(locator, config:)
53
+ [
54
+ XPath.attr(:id).equals(locator),
55
+ XPath.attr(:name).equals(locator),
56
+ XPath.attr(:value).is(locator),
57
+ XPath.attr(:title).is(locator),
58
+ (XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)),
59
+ (XPath.attr(:'aria-label').is(locator) if config.enable_aria_label),
60
+ (XPath.attr(test_id) == locator if config.test_id)
61
+ ].compact.inject(&:|)
62
+ end
51
63
  end
@@ -3,7 +3,7 @@
3
3
  Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
4
4
  css do |css|
5
5
  if css.is_a? Symbol
6
- warn "DEPRECATED: Passing a symbol (#{css.inspect}) as the CSS locator is deprecated - please pass a string instead."
6
+ Capybara::Helpers.warn "DEPRECATED: Passing a symbol (#{css.inspect}) as the CSS locator is deprecated - please pass a string instead : #{Capybara::Helpers.filter_backtrace(caller)}"
7
7
  end
8
8
  css
9
9
  end
@@ -19,7 +19,7 @@ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
19
19
 
20
20
  expression_filter(:with_options) do |expr, options|
21
21
  options.inject(expr) do |xpath, option|
22
- xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id)]
22
+ xpath.where(XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id))
23
23
  end
24
24
  end
25
25
 
@@ -18,7 +18,8 @@ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
18
18
  end
19
19
 
20
20
  describe_expression_filters do |**options|
21
- booleans, values = options.partition { |_k, v| [true, false].include? v }.map(&:to_h)
21
+ boolean_values = [true, false]
22
+ booleans, values = options.partition { |_k, v| boolean_values.include? v }.map(&:to_h)
22
23
  desc = describe_all_expression_filters(**values)
23
24
  desc + booleans.map do |k, v|
24
25
  v ? " with #{k} attribute" : "without #{k} attribute"
@@ -18,7 +18,7 @@ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
18
18
  end
19
19
  end
20
20
 
21
- filter_set(:_field, %i[disabled multiple name placeholder valid])
21
+ filter_set(:_field, %i[disabled multiple name placeholder valid validation_message])
22
22
 
23
23
  node_filter(:with) do |node, with|
24
24
  val = node.value
@@ -53,7 +53,7 @@ Capybara.add_selector(:label, locator_type: [String, Symbol]) do
53
53
  end
54
54
  end
55
55
  describe_node_filters do |**options|
56
- " for element #{options[:for]}" if options[:for]&.is_a?(Capybara::Node::Element)
56
+ " for element #{options[:for]}" if options[:for].is_a?(Capybara::Node::Element)
57
57
  end
58
58
 
59
59
  def labelable_elements
@@ -5,6 +5,13 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
5
5
  xpath = XPath.descendant(:a)
6
6
  xpath = builder(xpath).add_attribute_conditions(href: href) unless href == false
7
7
 
8
+ if enable_aria_role
9
+ role_path = XPath.descendant[XPath.attr(:role).equals('link')]
10
+ role_path = builder(role_path).add_attribute_conditions(href: href) unless [true, false].include? href
11
+
12
+ xpath += role_path
13
+ end
14
+
8
15
  unless locator.nil?
9
16
  locator = locator.to_s
10
17
  matchers = [XPath.attr(:id) == locator,
@@ -18,6 +25,7 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
18
25
 
19
26
  xpath = xpath[find_by_attr(:title, title)]
20
27
  xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
28
+
21
29
  xpath
22
30
  end
23
31
 
@@ -33,7 +33,7 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
33
33
 
34
34
  expression_filter(:with_options) do |expr, options|
35
35
  options.inject(expr) do |xpath, option|
36
- xpath[expression_for(:option, option)]
36
+ xpath.where(expression_for(:option, option))
37
37
  end
38
38
  end
39
39
 
@@ -43,7 +43,7 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
43
43
  end
44
44
 
45
45
  expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
46
- raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all? { |col| col.is_a? Array }
46
+ raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all?(Array)
47
47
 
48
48
  rows = cols.transpose
49
49
  col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
@@ -9,12 +9,12 @@ Capybara.add_selector(:table_row, locator_type: [Array, Hash]) do
9
9
  cell_xp = XPath.descendant(:td)[
10
10
  XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
11
11
  ]
12
- xp[cell_xp]
12
+ xp.where(cell_xp)
13
13
  end
14
14
  else
15
15
  initial_td = XPath.descendant(:td)[XPath.string.n.is(locator.shift)]
16
16
  tds = locator.reverse.map { |cell| XPath.following_sibling(:td)[XPath.string.n.is(cell)] }
17
- .reduce { |xp, cell| xp[cell] }
17
+ .reduce { |xp, cell| xp.where(cell) }
18
18
  xpath[initial_td[tds]]
19
19
  end
20
20
  end
@@ -12,7 +12,7 @@ module Capybara
12
12
  @node_filters = {}
13
13
  @expression_filters = {}
14
14
  @descriptions = Hash.new { |hsh, key| hsh[key] = [] }
15
- instance_eval(&block)
15
+ instance_eval(&block) if block
16
16
  end
17
17
 
18
18
  def node_filter(names, *types, **options, &block)
@@ -49,7 +49,7 @@ module Capybara
49
49
  end
50
50
 
51
51
  def descriptions
52
- warn 'DEPRECATED: FilterSet#descriptions is deprecated without replacement'
52
+ Capybara::Helpers.warn 'DEPRECATED: FilterSet#descriptions is deprecated without replacement'
53
53
  [undeclared_descriptions, node_filter_descriptions, expression_filter_descriptions].flatten
54
54
  end
55
55
 
@@ -48,6 +48,10 @@ module Capybara
48
48
  @config[:enable_aria_label]
49
49
  end
50
50
 
51
+ def enable_aria_role
52
+ @config[:enable_aria_role]
53
+ end
54
+
51
55
  def test_id
52
56
  @config[:test_id]
53
57
  end
@@ -128,7 +132,11 @@ module Capybara
128
132
  attr_matchers |= XPath.attr(test_id) == locator if test_id
129
133
 
130
134
  locate_xpath = locate_xpath[attr_matchers]
131
- locate_xpath + XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
135
+ locate_xpath + locate_label(locator).descendant(xpath)
136
+ end
137
+
138
+ def locate_label(locator)
139
+ XPath.descendant(:label)[XPath.string.n.is(locator)]
132
140
  end
133
141
 
134
142
  def find_by_attr(attribute, value)
@@ -158,7 +158,7 @@
158
158
  // the overflow style of the body, and the body is really overflow:visible.
159
159
  var overflowElem = e;
160
160
  if (htmlOverflowStyle == "visible") {
161
- // Note: bodyElem will be null/undefined in SVG documents.
161
+ // NOTE: bodyElem will be null/undefined in SVG documents.
162
162
  if (e == htmlElem && bodyElem) {
163
163
  overflowElem = bodyElem;
164
164
  } else if (e == bodyElem) {
@@ -12,20 +12,44 @@ 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')
16
+
15
17
  attr_reader :app, :options
16
18
 
17
19
  class << self
20
+ attr_reader :selenium_webdriver_version
21
+
18
22
  def load_selenium
19
23
  require 'selenium-webdriver'
20
24
  require 'capybara/selenium/logger_suppressor'
21
25
  require 'capybara/selenium/patches/atoms'
22
26
  require 'capybara/selenium/patches/is_displayed'
23
27
  require 'capybara/selenium/patches/action_pauser'
24
- if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
28
+
29
+ # Look up the version of `selenium-webdriver` to
30
+ # see if it's a version we support.
31
+ #
32
+ # By default, we use Gem.loaded_specs to determine
33
+ # the version number. However, in some cases, such
34
+ # as when loading `selenium-webdriver` outside of
35
+ # Rubygems, we fall back to referencing
36
+ # Selenium::WebDriver::VERSION. Ideally we'd
37
+ # use the constant in all cases, but earlier versions
38
+ # of `selenium-webdriver` didn't provide the constant.
39
+ @selenium_webdriver_version =
40
+ if Gem.loaded_specs['selenium-webdriver']
41
+ Gem.loaded_specs['selenium-webdriver'].version
42
+ else
43
+ Gem::Version.new(Selenium::WebDriver::VERSION)
44
+ end
45
+
46
+ unless Gem::Requirement.new('>= 3.5.0').satisfied_by? @selenium_webdriver_version
25
47
  warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
26
48
  end
49
+
50
+ @selenium_webdriver_version
27
51
  rescue LoadError => e
28
- raise e unless e.message.match?(/selenium-webdriver/)
52
+ raise e unless e.message.include?('selenium-webdriver')
29
53
 
30
54
  raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
31
55
  end
@@ -49,7 +73,15 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
49
73
  end
50
74
  end
51
75
  processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
52
- @browser = Selenium::WebDriver.for(options[:browser], processed_options)
76
+
77
+ @browser = if options[:browser] == :firefox &&
78
+ RUBY_VERSION >= '3.0' &&
79
+ Capybara::Selenium::Driver.selenium_webdriver_version <= Gem::Version.new('4.0.0.alpha1')
80
+ # selenium-webdriver 3.x doesn't correctly pass options through for Firefox with Ruby 3 so workaround that
81
+ Selenium::WebDriver::Firefox::Driver.new(**processed_options)
82
+ else
83
+ Selenium::WebDriver.for(options[:browser], processed_options)
84
+ end
53
85
 
54
86
  specialize_driver
55
87
  setup_exit_handler
@@ -58,6 +90,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
58
90
  end
59
91
 
60
92
  def initialize(app, **options)
93
+ super()
61
94
  self.class.load_selenium
62
95
  @app = app
63
96
  @browser = nil
@@ -85,6 +118,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
85
118
 
86
119
  def html
87
120
  browser.page_source
121
+ rescue Selenium::WebDriver::Error::JavascriptError => e
122
+ raise unless e.message.include?('documentElement is null')
88
123
  end
89
124
 
90
125
  def title
@@ -113,6 +148,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
113
148
  unwrap_script_result(result)
114
149
  end
115
150
 
151
+ def send_keys(*args)
152
+ active_element.send_keys(*args)
153
+ end
154
+
116
155
  def save_screenshot(path, **_options)
117
156
  browser.save_screenshot(path)
118
157
  end
@@ -241,7 +280,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
241
280
 
242
281
  def quit
243
282
  @browser&.quit
244
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/SuppressedException
283
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED,
284
+ Selenium::WebDriver::Error::InvalidSessionIdError
245
285
  # Browser must have already gone
246
286
  rescue Selenium::WebDriver::Error::UnknownError => e
247
287
  unless silenced_unknown_error_message?(e.message) # Most likely already gone
@@ -293,7 +333,7 @@ private
293
333
  def clear_browser_state
294
334
  delete_all_cookies
295
335
  clear_storage
296
- rescue *clear_browser_state_errors # rubocop:disable Lint/SuppressedException
336
+ rescue *clear_browser_state_errors
297
337
  # delete_all_cookies fails when we've previously gone
298
338
  # to about:blank, so we rescue this error and do nothing
299
339
  # instead.
@@ -317,7 +357,7 @@ private
317
357
  def clear_storage
318
358
  clear_session_storage unless options[:clear_session_storage] == false
319
359
  clear_local_storage unless options[:clear_local_storage] == false
320
- rescue Selenium::WebDriver::Error::JavascriptError # rubocop:disable Lint/SuppressedException
360
+ rescue Selenium::WebDriver::Error::JavascriptError
321
361
  # session/local storage may not be available if on non-http pages (e.g. about:blank)
322
362
  end
323
363
 
@@ -353,7 +393,7 @@ private
353
393
  @browser.navigate.to(url)
354
394
  sleep 0.1 # slight wait for alert
355
395
  @browser.switch_to.alert.accept
356
- rescue modal_error # rubocop:disable Lint/SuppressedException
396
+ rescue modal_error
357
397
  # alert now gone, should mean navigation happened
358
398
  end
359
399
 
@@ -435,6 +475,10 @@ private
435
475
  browser
436
476
  end
437
477
 
478
+ def active_element
479
+ browser.switch_to.active_element
480
+ end
481
+
438
482
  def build_node(native_node, initial_cache = {})
439
483
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
440
484
  end
@@ -7,27 +7,25 @@ module Capybara::Selenium::Driver::ChromeDriver
7
7
  def self.extended(base)
8
8
  bridge = base.send(:bridge)
9
9
  bridge.extend Capybara::Selenium::ChromeLogs unless bridge.respond_to?(:log)
10
- bridge.extend Capybara::Selenium::IsDisplayed unless bridge.commands(:is_element_displayed)
10
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
11
11
  base.options[:native_displayed] = false if base.options[:native_displayed].nil?
12
12
  end
13
13
 
14
14
  def fullscreen_window(handle)
15
15
  within_given_window(handle) do
16
- begin
17
- super
18
- rescue NoMethodError => e
19
- raise unless e.message.match?(/full_screen_window/)
20
-
21
- result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
22
- result['value']
23
- end
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.include?('full_screen_window')
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
24
22
  end
25
23
  end
26
24
 
27
25
  def resize_window_to(handle, width, height)
28
26
  super
29
27
  rescue Selenium::WebDriver::Error::UnknownError => e
30
- raise unless e.message.match?(/failed to change window state/)
28
+ raise unless e.message.include?('failed to change window state')
31
29
 
32
30
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
33
31
  # and raises unnecessary error. Wait a bit and try again.
@@ -65,7 +63,7 @@ private
65
63
  end
66
64
 
67
65
  def clear_all_storage?
68
- storage_clears.none? { |s| s == false }
66
+ storage_clears.none? false
69
67
  end
70
68
 
71
69
  def uniform_storage_clear?