capybara 3.31.0 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
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