capybara 3.31.0 → 3.33.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +51 -0
  3. data/README.md +10 -3
  4. data/lib/capybara.rb +17 -7
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/minitest.rb +215 -141
  7. data/lib/capybara/minitest/spec.rb +153 -97
  8. data/lib/capybara/node/actions.rb +16 -20
  9. data/lib/capybara/node/element.rb +2 -0
  10. data/lib/capybara/node/matchers.rb +4 -6
  11. data/lib/capybara/queries/selector_query.rb +8 -1
  12. data/lib/capybara/queries/style_query.rb +1 -1
  13. data/lib/capybara/queries/text_query.rb +6 -0
  14. data/lib/capybara/rack_test/browser.rb +3 -1
  15. data/lib/capybara/registration_container.rb +44 -0
  16. data/lib/capybara/registrations/servers.rb +1 -1
  17. data/lib/capybara/result.rb +5 -1
  18. data/lib/capybara/rspec/matcher_proxies.rb +4 -4
  19. data/lib/capybara/rspec/matchers/have_text.rb +1 -1
  20. data/lib/capybara/selector.rb +10 -1
  21. data/lib/capybara/selector/definition.rb +5 -4
  22. data/lib/capybara/selector/definition/button.rb +1 -0
  23. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  24. data/lib/capybara/selector/definition/link.rb +8 -0
  25. data/lib/capybara/selector/definition/table.rb +1 -1
  26. data/lib/capybara/selector/selector.rb +4 -0
  27. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  28. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  29. data/lib/capybara/selenium/driver.rb +7 -4
  30. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +7 -9
  31. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +7 -9
  32. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  33. data/lib/capybara/selenium/node.rb +69 -9
  34. data/lib/capybara/selenium/nodes/chrome_node.rb +0 -9
  35. data/lib/capybara/selenium/nodes/firefox_node.rb +2 -2
  36. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  37. data/lib/capybara/selenium/patches/logs.rb +3 -5
  38. data/lib/capybara/session.rb +3 -3
  39. data/lib/capybara/session/config.rb +3 -1
  40. data/lib/capybara/spec/public/test.js +18 -0
  41. data/lib/capybara/spec/session/click_button_spec.rb +11 -0
  42. data/lib/capybara/spec/session/fill_in_spec.rb +9 -0
  43. data/lib/capybara/spec/session/find_spec.rb +11 -8
  44. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  45. data/lib/capybara/spec/session/has_css_spec.rb +9 -6
  46. data/lib/capybara/spec/session/has_current_path_spec.rb +2 -2
  47. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  48. data/lib/capybara/spec/session/has_select_spec.rb +4 -4
  49. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  50. data/lib/capybara/spec/session/node_spec.rb +54 -27
  51. data/lib/capybara/spec/session/window/window_spec.rb +7 -7
  52. data/lib/capybara/spec/spec_helper.rb +2 -2
  53. data/lib/capybara/spec/test_app.rb +14 -18
  54. data/lib/capybara/spec/views/form.erb +7 -1
  55. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  56. data/lib/capybara/spec/views/with_html.erb +2 -2
  57. data/lib/capybara/spec/views/with_js.erb +1 -0
  58. data/lib/capybara/version.rb +1 -1
  59. data/spec/capybara_spec.rb +1 -1
  60. data/spec/dsl_spec.rb +14 -1
  61. data/spec/minitest_spec.rb +1 -1
  62. data/spec/rack_test_spec.rb +13 -0
  63. data/spec/regexp_dissassembler_spec.rb +0 -4
  64. data/spec/result_spec.rb +38 -31
  65. data/spec/rspec/shared_spec_matchers.rb +65 -53
  66. data/spec/selector_spec.rb +1 -1
  67. data/spec/selenium_spec_chrome.rb +4 -2
  68. data/spec/selenium_spec_chrome_remote.rb +2 -0
  69. data/spec/server_spec.rb +41 -49
  70. data/spec/shared_selenium_node.rb +18 -0
  71. data/spec/shared_selenium_session.rb +25 -7
  72. data/spec/spec_helper.rb +1 -1
  73. metadata +5 -3
@@ -34,7 +34,7 @@ module Capybara
34
34
  private
35
35
 
36
36
  def stringify_keys(hsh)
37
- hsh.each_with_object({}) { |(k, v), str_keys| str_keys[k.to_s] = v }
37
+ hsh.transform_keys(&:to_s)
38
38
  end
39
39
 
40
40
  def valid_keys
@@ -6,6 +6,8 @@ module Capybara
6
6
  class TextQuery < BaseQuery
7
7
  def initialize(type = nil, expected_text, session_options:, **options) # rubocop:disable Style/OptionalArguments
8
8
  @type = type.nil? ? default_type : type
9
+ raise ArgumentError, '${@type} is not a valid type for a text query' unless valid_types.include?(@type)
10
+
9
11
  @options = options
10
12
  super(@options)
11
13
  self.session_options = session_options
@@ -89,6 +91,10 @@ module Capybara
89
91
  COUNT_KEYS + %i[wait exact normalize_ws]
90
92
  end
91
93
 
94
+ def valid_types
95
+ %i[all visible]
96
+ end
97
+
92
98
  def check_visible_text?
93
99
  @type == :visible
94
100
  end
@@ -30,7 +30,9 @@ class Capybara::RackTest::Browser
30
30
 
31
31
  def submit(method, path, attributes)
32
32
  path = request_path if path.nil? || path.empty?
33
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
33
+ uri = build_uri(path)
34
+ uri.query = '' if method&.to_s&.downcase == 'get'
35
+ process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => current_url)
34
36
  end
35
37
 
36
38
  def follow(method, path, **attributes)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ # @api private
5
+ class RegistrationContainer
6
+ def names
7
+ @registered.keys
8
+ end
9
+
10
+ def [](name)
11
+ @registered[name]
12
+ end
13
+
14
+ def []=(name, value)
15
+ warn 'DEPRECATED: Directly setting drivers/servers is deprecated, please use Capybara.register_driver/register_server instead'
16
+ @registered[name] = value
17
+ end
18
+
19
+ def method_missing(method_name, *args, **options, &block)
20
+ if @registered.respond_to?(method_name)
21
+ 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
+ return @registered.public_send(method_name, *args, **options, &block)
26
+ end
27
+ super
28
+ end
29
+
30
+ def respond_to_missing?(method_name, include_private = false)
31
+ @registered.respond_to?(method_name) || super
32
+ end
33
+
34
+ private
35
+
36
+ def initialize
37
+ @registered = {}
38
+ end
39
+
40
+ def register(name, block)
41
+ @registered[name] = block
42
+ end
43
+ end
44
+ end
@@ -7,7 +7,7 @@ end
7
7
  Capybara.register_server :webrick do |app, port, host, **options|
8
8
  require 'rack/handler/webrick'
9
9
  options = { Host: host, Port: port, AccessLog: [], Logger: WEBrick::Log.new(nil, 0) }.merge(options)
10
- Rack::Handler::WEBrick.run(app, options)
10
+ Rack::Handler::WEBrick.run(app, **options)
11
11
  end
12
12
 
13
13
  Capybara.register_server :puma do |app, port, host, **options|
@@ -63,6 +63,7 @@ module Capybara
63
63
  # idx.max is broken with beginless ranges
64
64
  # idx.end && idx.max # endless range will have end == nil
65
65
  max = idx.end
66
+ max = nil if max&.negative?
66
67
  max -= 1 if max && idx.exclude_end?
67
68
  max
68
69
  end
@@ -170,10 +171,13 @@ module Capybara
170
171
  @rest ||= @elements - full_results
171
172
  end
172
173
 
173
- if (RUBY_PLATFORM == 'java') && (Gem::Version.new(JRUBY_VERSION) < Gem::Version.new('9.2.8.0'))
174
+ if RUBY_PLATFORM == 'java'
174
175
  # JRuby < 9.2.8.0 has an issue with lazy enumerators which
175
176
  # causes a concurrency issue with network requests here
176
177
  # https://github.com/jruby/jruby/issues/4212
178
+ # while JRuby >= 9.2.8.0 leaks threads when using lazy enumerators
179
+ # https://github.com/teamcapybara/capybara/issues/2349
180
+ # so disable the use and JRuby users will need to pay a performance penalty
177
181
  def lazy_select_elements(&block)
178
182
  @elements.select(&block).to_enum # non-lazy evaluation
179
183
  end
@@ -2,17 +2,17 @@
2
2
 
3
3
  module Capybara
4
4
  module RSpecMatcherProxies
5
- def all(*args, &block)
5
+ def all(*args, **kwargs, &block)
6
6
  if defined?(::RSpec::Matchers::BuiltIn::All) && args.first.respond_to?(:matches?)
7
7
  ::RSpec::Matchers::BuiltIn::All.new(*args)
8
8
  else
9
- find_all(*args, &block)
9
+ find_all(*args, **kwargs, &block)
10
10
  end
11
11
  end
12
12
 
13
- def within(*args, &block)
13
+ def within(*args, **kwargs, &block)
14
14
  if block_given?
15
- within_element(*args, &block)
15
+ within_element(*args, **kwargs, &block)
16
16
  else
17
17
  be_within(*args)
18
18
  end
@@ -15,7 +15,7 @@ module Capybara
15
15
  end
16
16
 
17
17
  def description
18
- "text #{format(text)}"
18
+ "have text #{format(text)}"
19
19
  end
20
20
 
21
21
  def format(content)
@@ -40,6 +40,7 @@ require 'capybara/selector/definition'
40
40
  # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
41
41
  # * :multiple (Boolean) - Match fields that accept multiple values
42
42
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
43
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
43
44
  #
44
45
  # * **:fieldset** - Select fieldset elements
45
46
  # * Locator: Matches id, {Capybara.configure test_id}, or contents of wrapped legend
@@ -79,6 +80,7 @@ require 'capybara/selector/definition'
79
80
  # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
80
81
  # * :multiple (Boolean) - Match fields that accept multiple values
81
82
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
83
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
82
84
  #
83
85
  # * **:radio_button** - Find radio buttons
84
86
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
@@ -178,6 +180,12 @@ Capybara::Selector::FilterSet.add(:_field) do
178
180
  node_filter(:valid, :boolean) { |node, value| node.evaluate_script('this.validity.valid') == value }
179
181
  node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
180
182
  node_filter(:placeholder) { |node, value| !value.is_a?(Regexp) || value.match?(node[:placeholder]) }
183
+ node_filter(:validation_message) do |node, msg|
184
+ vm = node[:validationMessage]
185
+ (msg.is_a?(Regexp) ? msg.match?(vm) : vm == msg.to_s).tap do |res|
186
+ add_error("Expected validation message to be #{msg.inspect} but was #{vm}") unless res
187
+ end
188
+ end
181
189
 
182
190
  expression_filter(:name) do |xpath, val|
183
191
  builder(xpath).add_attribute_conditions(name: val)
@@ -198,7 +206,7 @@ Capybara::Selector::FilterSet.add(:_field) do
198
206
  desc
199
207
  end
200
208
 
201
- describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, valid: nil, **|
209
+ describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, valid: nil, validation_message: nil, **|
202
210
  desc, states = +'', []
203
211
  states << 'checked' if checked || (unchecked == false)
204
212
  states << 'not checked' if unchecked || (checked == false)
@@ -206,6 +214,7 @@ Capybara::Selector::FilterSet.add(:_field) do
206
214
  desc << " that is #{states.join(' and ')}" unless states.empty?
207
215
  desc << ' that is valid' if valid == true
208
216
  desc << ' that is invalid' if valid == false
217
+ desc << " with validation message #{validation_message.to_s.inspect}" if validation_message
209
218
  desc
210
219
  end
211
220
  end
@@ -10,6 +10,7 @@ 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)
@@ -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
@@ -251,9 +252,9 @@ 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
 
@@ -4,6 +4,7 @@ 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
+ btn_xpath += XPath.descendant[XPath.attr(:role).equals('button')] if enable_aria_role
7
8
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
8
9
 
9
10
  unless locator.nil?
@@ -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
@@ -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
 
@@ -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(:&)
@@ -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
@@ -1 +1 @@
1
- (function(){function u(e){var t=e.tagName.toUpperCase();if("OPTION"==t)return!0;if("INPUT"!=t)return!1;var r=e.type.toLowerCase();return"checkbox"==r||"radio"==r}function s(e){var t="selected",r=e.type&&e.type.toLowerCase();return"checkbox"!=r&&"radio"!=r||(t="checked"),!!e[t]}function c(e,t){var r=e.getAttributeNode(t);return r&&r.specified?r.value:null}var i=["allowfullscreen","allowpaymentrequest","allowusermedia","async","autofocus","autoplay","checked","compact","complete","controls","declare","default","defaultchecked","defaultselected","defer","disabled","ended","formnovalidate","hidden","indeterminate","iscontenteditable","ismap","itemscope","loop","multiple","muted","nohref","nomodule","noresize","noshade","novalidate","nowrap","open","paused","playsinline","pubdate","readonly","required","reversed","scoped","seamless","seeking","selected","truespeed","typemustmatch","willvalidate"],d={"class":"className",readonly:"readOnly"};return function f(e,t){var r=null,a=t.toLowerCase();if("style"==a)return(r=e.style)&&"string"!=typeof r&&(r=r.cssText),r;if(("selected"==a||"checked"==a)&&u(e))return s(e)?"true":null;if(tagName=e.tagName.toUpperCase(),"IMG"==tagName&&"src"==a||"A"==tagName&&"href"==a)return(r=c(e,a))&&(r=e[a]),r;if("spellcheck"==a){if(null===!(r=c(e,a))){if("false"==r.toLowerCase())return"false";if("true"==r.toLowerCase())return"true"}return e[a]+""}var l,n=d[t]||t;if(i.some(function(e){e==a}))return(r=!(null===(r=c(e,a)))||e[n])?"true":null;try{l=e[n]}catch(o){}return null!=(r=null==l||"object"==typeof l||"function"==typeof l?c(e,t):l)?r.toString():null}})()
1
+ (function(){function u(e){var t=e.tagName.toUpperCase();if("OPTION"==t)return!0;if("INPUT"!=t)return!1;var r=e.type.toLowerCase();return"checkbox"==r||"radio"==r}function s(e){var t="selected",r=e.type&&e.type.toLowerCase();return"checkbox"!=r&&"radio"!=r||(t="checked"),!!e[t]}function c(e,t){var r=e.getAttributeNode(t);return r&&r.specified?r.value:null}var i=["allowfullscreen","allowpaymentrequest","allowusermedia","async","autofocus","autoplay","checked","compact","complete","controls","declare","default","defaultchecked","defaultselected","defer","disabled","ended","formnovalidate","hidden","indeterminate","iscontenteditable","ismap","itemscope","loop","multiple","muted","nohref","nomodule","noresize","noshade","novalidate","nowrap","open","paused","playsinline","pubdate","readonly","required","reversed","scoped","seamless","seeking","selected","truespeed","typemustmatch","willvalidate"],d={"class":"className",readonly:"readOnly"};return function f(e,t){var r=null,a=t.toLowerCase();if("style"==a)return(r=e.style)&&"string"!=typeof r&&(r=r.cssText),r;if(("selected"==a||"checked"==a)&&u(e))return s(e)?"true":null;if(tagName=e.tagName.toUpperCase(),"IMG"==tagName&&"src"==a||"A"==tagName&&"href"==a)return(r=c(e,a))&&(r=e[a]),r;if("spellcheck"==a){if(null!==(r=c(e,a))){if("false"==r.toLowerCase())return"false";if("true"==r.toLowerCase())return"true"}return e[a]+""}var l,n=d[t]||t;if(i.some(function(e){e==a}))return(r=!(null===(r=c(e,a)))||e[n])?"true":null;try{l=e[n]}catch(o){}return null!=(r=null==l||"object"==typeof l||"function"==typeof l?c(e,t):l)?r.toString():null}})()
@@ -117,7 +117,7 @@
117
117
 
118
118
  if ("spellcheck" == name) {
119
119
  value = getAttributeValue(element, name);
120
- if (!value === null) {
120
+ if (!(value === null)) {
121
121
  if (value.toLowerCase() == "false") {
122
122
  return "false";
123
123
  } else if (value.toLowerCase() == "true") {
@@ -20,6 +20,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
20
20
  require 'capybara/selenium/logger_suppressor'
21
21
  require 'capybara/selenium/patches/atoms'
22
22
  require 'capybara/selenium/patches/is_displayed'
23
+ require 'capybara/selenium/patches/action_pauser'
23
24
  if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
24
25
  warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
25
26
  end
@@ -84,6 +85,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
84
85
 
85
86
  def html
86
87
  browser.page_source
88
+ rescue Selenium::WebDriver::Error::JavascriptError => e
89
+ raise unless e.message.match?(/documentElement is null/)
87
90
  end
88
91
 
89
92
  def title
@@ -240,7 +243,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
240
243
 
241
244
  def quit
242
245
  @browser&.quit
243
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/SuppressedException
246
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
244
247
  # Browser must have already gone
245
248
  rescue Selenium::WebDriver::Error::UnknownError => e
246
249
  unless silenced_unknown_error_message?(e.message) # Most likely already gone
@@ -292,7 +295,7 @@ private
292
295
  def clear_browser_state
293
296
  delete_all_cookies
294
297
  clear_storage
295
- rescue *clear_browser_state_errors # rubocop:disable Lint/SuppressedException
298
+ rescue *clear_browser_state_errors
296
299
  # delete_all_cookies fails when we've previously gone
297
300
  # to about:blank, so we rescue this error and do nothing
298
301
  # instead.
@@ -316,7 +319,7 @@ private
316
319
  def clear_storage
317
320
  clear_session_storage unless options[:clear_session_storage] == false
318
321
  clear_local_storage unless options[:clear_local_storage] == false
319
- rescue Selenium::WebDriver::Error::JavascriptError # rubocop:disable Lint/SuppressedException
322
+ rescue Selenium::WebDriver::Error::JavascriptError
320
323
  # session/local storage may not be available if on non-http pages (e.g. about:blank)
321
324
  end
322
325
 
@@ -352,7 +355,7 @@ private
352
355
  @browser.navigate.to(url)
353
356
  sleep 0.1 # slight wait for alert
354
357
  @browser.switch_to.alert.accept
355
- rescue modal_error # rubocop:disable Lint/SuppressedException
358
+ rescue modal_error
356
359
  # alert now gone, should mean navigation happened
357
360
  end
358
361
 
@@ -13,14 +13,12 @@ module Capybara::Selenium::Driver::ChromeDriver
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.match?(/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
 
@@ -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?
@@ -13,14 +13,12 @@ module Capybara::Selenium::Driver::EdgeDriver
13
13
  return super if edgedriver_version < 75
14
14
 
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.match?(/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
 
@@ -74,7 +72,7 @@ private
74
72
  end
75
73
 
76
74
  def clear_all_storage?
77
- storage_clears.none? { |s| s == false }
75
+ storage_clears.none? false
78
76
  end
79
77
 
80
78
  def uniform_storage_clear?
@@ -46,7 +46,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
46
46
  begin
47
47
  # Firefox 68 hangs if we try to switch windows while a modal is visible
48
48
  browser.switch_to.alert&.dismiss
49
- rescue Selenium::WebDriver::Error::NoSuchAlertError # rubocop:disable Lint/SuppressedException
49
+ rescue Selenium::WebDriver::Error::NoSuchAlertError
50
50
  # Swallow
51
51
  end
52
52
  end
@@ -61,7 +61,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
61
61
  accept_modal :confirm, wait: 0.1 do
62
62
  super
63
63
  end
64
- rescue Capybara::ModalNotFound # rubocop:disable Lint/SuppressedException
64
+ rescue Capybara::ModalNotFound
65
65
  # No modal was opened - page has refreshed - ignore
66
66
  end
67
67
 
@@ -104,7 +104,20 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
104
104
  click_options = ClickOptions.new(keys, options)
105
105
  return native.click if click_options.empty?
106
106
 
107
- click_with_options(click_options)
107
+ perform_with_options(click_options) do |action|
108
+ target = click_options.coords? ? nil : native
109
+ if click_options.delay.zero?
110
+ action.click(target)
111
+ else
112
+ action.click_and_hold(target)
113
+ if w3c?
114
+ action.pause(action.pointer_inputs.first, click_options.delay)
115
+ else
116
+ action.pause(click_options.delay)
117
+ end
118
+ action.release
119
+ end
120
+ end
108
121
  rescue StandardError => e
109
122
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
110
123
  e.message.match?(/Other element would receive the click/)
@@ -116,14 +129,26 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
116
129
 
117
130
  def right_click(keys = [], **options)
118
131
  click_options = ClickOptions.new(keys, options)
119
- click_with_options(click_options) do |action|
120
- click_options.coords? ? action.context_click : action.context_click(native)
132
+ perform_with_options(click_options) do |action|
133
+ target = click_options.coords? ? nil : native
134
+ if click_options.delay.zero?
135
+ action.context_click(target)
136
+ elsif w3c?
137
+ action.move_to(target) if target
138
+ action.pointer_down(:right)
139
+ .pause(action.pointer_inputs.first, click_options.delay)
140
+ .pointer_up(:right)
141
+ else
142
+ raise ArgumentError, 'Delay is not supported when right clicking with legacy (non-w3c) selenium driver'
143
+ end
121
144
  end
122
145
  end
123
146
 
124
147
  def double_click(keys = [], **options)
125
148
  click_options = ClickOptions.new(keys, options)
126
- click_with_options(click_options) do |action|
149
+ raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?
150
+
151
+ perform_with_options(click_options) do |action|
127
152
  click_options.coords? ? action.double_click : action.double_click(native)
128
153
  end
129
154
  end
@@ -212,7 +237,7 @@ protected
212
237
  JS
213
238
  begin
214
239
  driver.execute_script(script, self)
215
- rescue StandardError # rubocop:disable Lint/SuppressedException
240
+ rescue StandardError
216
241
  # Swallow error if scrollIntoView with options isn't supported
217
242
  end
218
243
  end
@@ -242,7 +267,7 @@ private
242
267
  find_xpath(XPath.ancestor(:select)[1]).first
243
268
  end
244
269
 
245
- def set_text(value, clear: nil, **_unused)
270
+ def set_text(value, clear: nil, rapid: nil, **_unused)
246
271
  value = value.to_s
247
272
  if value.empty? && clear.nil?
248
273
  native.clear
@@ -254,11 +279,23 @@ private
254
279
  send_keys(*clear, value)
255
280
  else
256
281
  driver.execute_script 'arguments[0].select()', self unless clear == :none
257
- send_keys(value)
282
+ if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
283
+ send_keys(value[0..3])
284
+ driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
285
+ send_keys(value[-3..-1])
286
+ else
287
+ send_keys(value)
288
+ end
258
289
  end
259
290
  end
260
291
 
261
- def click_with_options(click_options)
292
+ def auto_rapid_set_length
293
+ 30
294
+ end
295
+
296
+ def perform_with_options(click_options, &block)
297
+ raise ArgumentError, 'A block must be provided' unless block
298
+
262
299
  scroll_if_needed do
263
300
  action_with_modifiers(click_options) do |action|
264
301
  if block_given?
@@ -407,6 +444,15 @@ private
407
444
  browser.action
408
445
  end
409
446
 
447
+ def capabilities
448
+ browser.capabilities
449
+ end
450
+
451
+ def w3c?
452
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
453
+ capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
454
+ end
455
+
410
456
  def normalize_keys(keys)
411
457
  keys.map do |key|
412
458
  case key
@@ -488,6 +534,16 @@ private
488
534
  })(arguments[0], arguments[1], arguments[2])
489
535
  JS
490
536
 
537
+ RAPID_APPEND_TEXT = <<~'JS'
538
+ (function(el, value) {
539
+ value = el.value + value;
540
+ if (el.maxLength && el.maxLength != -1){
541
+ value = value.slice(0, el.maxLength);
542
+ }
543
+ el.value = value;
544
+ })(arguments[0], arguments[1])
545
+ JS
546
+
491
547
  # SettableValue encapsulates time/date field formatting
492
548
  class SettableValue
493
549
  attr_reader :value
@@ -544,7 +600,11 @@ private
544
600
  end
545
601
 
546
602
  def empty?
547
- keys.empty? && !coords?
603
+ keys.empty? && !coords? && delay.zero?
604
+ end
605
+
606
+ def delay
607
+ options[:delay] || 0
548
608
  end
549
609
  end
550
610
  private_constant :ClickOptions