capybara 3.38.0 → 3.39.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +20 -0
  3. data/lib/capybara/node/actions.rb +4 -4
  4. data/lib/capybara/node/finders.rb +2 -0
  5. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  6. data/lib/capybara/rack_test/node.rb +18 -15
  7. data/lib/capybara/registrations/drivers.rb +3 -3
  8. data/lib/capybara/registrations/servers.rb +16 -4
  9. data/lib/capybara/rspec/matcher_proxies.rb +3 -3
  10. data/lib/capybara/rspec/matchers/base.rb +8 -6
  11. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  12. data/lib/capybara/selector/definition/link.rb +2 -1
  13. data/lib/capybara/selenium/driver.rb +4 -4
  14. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +8 -4
  15. data/lib/capybara/selenium/extensions/html5_drag.rb +3 -0
  16. data/lib/capybara/selenium/node.rb +8 -10
  17. data/lib/capybara/selenium/nodes/chrome_node.rb +5 -1
  18. data/lib/capybara/selenium/nodes/edge_node.rb +24 -2
  19. data/lib/capybara/selenium/patches/action_pauser.rb +3 -3
  20. data/lib/capybara/selenium/patches/atoms.rb +1 -1
  21. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  22. data/lib/capybara/server/animation_disabler.rb +2 -3
  23. data/lib/capybara/spec/public/test.js +4 -0
  24. data/lib/capybara/spec/session/all_spec.rb +1 -1
  25. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  26. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  27. data/lib/capybara/spec/session/find_spec.rb +1 -1
  28. data/lib/capybara/spec/session/first_spec.rb +1 -1
  29. data/lib/capybara/spec/session/frame/within_frame_spec.rb +2 -0
  30. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  31. data/lib/capybara/spec/session/has_any_selectors_spec.rb +2 -2
  32. data/lib/capybara/spec/session/has_current_path_spec.rb +1 -1
  33. data/lib/capybara/spec/session/has_link_spec.rb +5 -1
  34. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  35. data/lib/capybara/spec/session/matches_style_spec.rb +2 -0
  36. data/lib/capybara/spec/session/node_spec.rb +16 -0
  37. data/lib/capybara/spec/session/scroll_spec.rb +3 -1
  38. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  39. data/lib/capybara/spec/views/form.erb +4 -0
  40. data/lib/capybara/spec/views/with_html.erb +1 -1
  41. data/lib/capybara/version.rb +1 -1
  42. data/spec/css_builder_spec.rb +1 -1
  43. data/spec/css_splitter_spec.rb +1 -1
  44. data/spec/rack_test_spec.rb +2 -2
  45. data/spec/rspec/scenarios_spec.rb +1 -1
  46. data/spec/rspec_matchers_spec.rb +25 -0
  47. data/spec/sauce_spec_chrome.rb +1 -1
  48. data/spec/selenium_spec_chrome.rb +5 -6
  49. data/spec/selenium_spec_chrome_remote.rb +5 -7
  50. data/spec/selenium_spec_edge.rb +11 -7
  51. data/spec/selenium_spec_firefox.rb +10 -4
  52. data/spec/selenium_spec_firefox_remote.rb +16 -3
  53. data/spec/selenium_spec_ie.rb +1 -1
  54. data/spec/selenium_spec_safari.rb +1 -1
  55. data/spec/server_spec.rb +2 -2
  56. data/spec/shared_selenium_session.rb +2 -1
  57. data/spec/spec_helper.rb +33 -0
  58. data/spec/whitespace_normalizer_spec.rb +54 -0
  59. data/spec/xpath_builder_spec.rb +1 -1
  60. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 163cb499d23bf17278c9912462fb3fb337b2d28b764a95c5ed3f9c4af3f1c317
4
- data.tar.gz: 7010ef4e7f6af6c4d78ae58a05846c2607864d6fe24d05c49747fc1e1ded8675
3
+ metadata.gz: ab39ff6da58cbd3edd267cec393f613a79bbabd1092e7929ad32f352ee46c914
4
+ data.tar.gz: c61ad6d311f09fba5878e865133adfe5c8f16b65b3f1a184e2ef95655f7f7685
5
5
  SHA512:
6
- metadata.gz: 66ac2676f22d83ec1a4478f68a8f7f095c9c9f9396eb1b5c1bf34803e15e19ed02c1f1342731d55787219ddd2d9d0829eb0c76ef702c4e07ac3a0b19a9c2aaec
7
- data.tar.gz: f35c37f9ccdcdd58c1c76c80cf58eb8c62b0878bb40ff1dd6191978978409b41adabbc34013868e30a5ec4f33d077532ad8c1a90fdf0138dd524703c5168a001
6
+ metadata.gz: e449ab4b9d7750df25bc5a2c4afb0391f825fa27c2b0cd86b34f1b903245e28268b55265b4d9a066616bcd40ed3f598b96701873e83d7da1ebafb8eb6e8568cd
7
+ data.tar.gz: beeba4a6862e5e51c93cbf530e2353ac3d5862fe818f192c18411133a17b836e634495d8d58a8a7215c9f153ba3a56c92b7b506b951868012b8661b05eeb5381
data/History.md CHANGED
@@ -1,3 +1,23 @@
1
+ # Version 3.39.0
2
+ Release date: 2023-04-02
3
+
4
+ ### Added
5
+
6
+ * Support `:target` filter option on `:link` selector [Yudai Takada]
7
+ * Experimental Rack 3 support
8
+ * Text normalization performance improvements [Brandon Weaver]
9
+
10
+ ### Fixed
11
+
12
+ * MS Edge button click [Brian J. Bayer]
13
+ * Options/Capabilities choosing based on Selenium versions
14
+ * Support for base versions [Matijs van Zuijlen]
15
+ * ExpectedError not defined in Selenium 4+
16
+ * Filter block forwarding to a number of matchers [Christophe Bliard]
17
+ ### Changed
18
+
19
+ * Dropped support for rack 1.x
20
+
1
21
  # Version 3.38.0
2
22
  Release date: 2022-11-03
3
23
 
@@ -383,7 +383,7 @@ module Capybara
383
383
  end
384
384
  end
385
385
 
386
- UPDATE_STYLE_SCRIPT = <<~'JS'
386
+ UPDATE_STYLE_SCRIPT = <<~JS
387
387
  this.capybara_style_cache = this.style.cssText;
388
388
  var css = arguments[0];
389
389
  for (var prop in css){
@@ -393,20 +393,20 @@ module Capybara
393
393
  }
394
394
  JS
395
395
 
396
- RESET_STYLE_SCRIPT = <<~'JS'
396
+ RESET_STYLE_SCRIPT = <<~JS
397
397
  if (this.hasOwnProperty('capybara_style_cache')) {
398
398
  this.style.cssText = this.capybara_style_cache;
399
399
  delete this.capybara_style_cache;
400
400
  }
401
401
  JS
402
402
 
403
- DATALIST_OPTIONS_SCRIPT = <<~'JS'
403
+ DATALIST_OPTIONS_SCRIPT = <<~JS
404
404
  Array.prototype.slice.call((this.list||{}).options || []).
405
405
  filter(function(el){ return !el.disabled }).
406
406
  map(function(el){ return { "value": el.value, "label": el.label} })
407
407
  JS
408
408
 
409
- CAPTURE_FILE_ELEMENT_SCRIPT = <<~'JS'
409
+ CAPTURE_FILE_ELEMENT_SCRIPT = <<~JS
410
410
  document.addEventListener('click', function file_catcher(e){
411
411
  if (e.target.matches("input[type='file']")) {
412
412
  window._capybara_clicked_file_input = e.target;
@@ -149,6 +149,8 @@ module Capybara
149
149
  # @option options [String, Regexp] id Match links with the id provided
150
150
  # @option options [String] title Match links with the title provided
151
151
  # @option options [String] alt Match links with a contained img element whose alt matches
152
+ # @option options [String, Boolean] download Match links with the download provided
153
+ # @option options [String] target Match links with the target provided
152
154
  # @option options [String, Array<String>, Regexp] class Match links that match the class(es) provided
153
155
  # @return [Capybara::Node::Element] The found element
154
156
  #
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Node
5
+ ##
6
+ #
7
+ # {Capybara::Node::WhitespaceNormalizer} provides methods that
8
+ # help to normalize the spacing of text content inside of
9
+ # {Capybara::Node::Element}s by removing various unicode
10
+ # spacing and directional markings.
11
+ #
12
+ module WhitespaceNormalizer
13
+ # Unicode for NBSP, or &nbsp;
14
+ NON_BREAKING_SPACE = "\u00a0"
15
+ LINE_SEPERATOR = "\u2028"
16
+ PARAGRAPH_SEPERATOR = "\u2029"
17
+
18
+ # All spaces except for NBSP
19
+ BREAKING_SPACES = "[[:space:]&&[^#{NON_BREAKING_SPACE}]]"
20
+
21
+ # Whitespace we want to substitute with plain spaces
22
+ SQUEEZED_SPACES = " \n\f\t\v#{LINE_SEPERATOR}#{PARAGRAPH_SEPERATOR}"
23
+
24
+ # Any whitespace at the front of text
25
+ LEADING_SPACES = /\A#{BREAKING_SPACES}+/.freeze
26
+
27
+ # Any whitespace at the end of text
28
+ TRAILING_SPACES = /#{BREAKING_SPACES}+\z/.freeze
29
+
30
+ # "Invisible" space character
31
+ ZERO_WIDTH_SPACE = "\u200b"
32
+
33
+ # Signifies text is read left to right
34
+ LEFT_TO_RIGHT_MARK = "\u200e"
35
+
36
+ # Signifies text is read right to left
37
+ RIGHT_TO_LEFT_MARK = "\u200f"
38
+
39
+ # Characters we want to truncate from text
40
+ REMOVED_CHARACTERS = [ZERO_WIDTH_SPACE, LEFT_TO_RIGHT_MARK, RIGHT_TO_LEFT_MARK].join
41
+
42
+ # Matches multiple empty lines
43
+ EMPTY_LINES = /[\ \n]*\n[\ \n]*/.freeze
44
+
45
+ ##
46
+ #
47
+ # Normalizes the spacing of a node's text to be similar to
48
+ # what matchers might expect.
49
+ #
50
+ # @param text [String]
51
+ # @return [String]
52
+ #
53
+ def normalize_spacing(text)
54
+ text
55
+ .delete(REMOVED_CHARACTERS)
56
+ .tr(SQUEEZED_SPACES, ' ')
57
+ .squeeze(' ')
58
+ .sub(LEADING_SPACES, '')
59
+ .sub(TRAILING_SPACES, '')
60
+ .tr(NON_BREAKING_SPACE, ' ')
61
+ end
62
+
63
+ ##
64
+ #
65
+ # Variant on {Capybara::Node::Normalizer#normalize_spacing} that
66
+ # targets the whitespace of visible elements only.
67
+ #
68
+ # @param text [String]
69
+ # @return [String]
70
+ #
71
+ def normalize_visible_spacing(text)
72
+ text
73
+ .squeeze(' ')
74
+ .gsub(EMPTY_LINES, "\n")
75
+ .sub(LEADING_SPACES, '')
76
+ .sub(TRAILING_SPACES, '')
77
+ .tr(NON_BREAKING_SPACE, ' ')
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,25 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rack_test/errors'
4
+ require 'capybara/node/whitespace_normalizer'
4
5
 
5
6
  class Capybara::RackTest::Node < Capybara::Driver::Node
7
+ include Capybara::Node::WhitespaceNormalizer
8
+
6
9
  BLOCK_ELEMENTS = %w[p h1 h2 h3 h4 h5 h6 ol ul pre address blockquote dl div fieldset form hr noscript table].freeze
7
10
 
8
11
  def all_text
9
- native.text
10
- .gsub(/[\u200b\u200e\u200f]/, '')
11
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
12
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
13
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
14
- .tr("\u00a0", ' ')
12
+ normalize_spacing(native.text)
15
13
  end
16
14
 
17
15
  def visible_text
18
- displayed_text.squeeze(' ')
19
- .gsub(/[\ \n]*\n[\ \n]*/, "\n")
20
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
21
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
22
- .tr("\u00a0", ' ')
16
+ normalize_visible_spacing(displayed_text)
23
17
  end
24
18
 
25
19
  def [](name)
@@ -153,9 +147,11 @@ protected
153
147
  if !string_node.visible?(check_ancestor)
154
148
  ''
155
149
  elsif native.text?
156
- native.text
157
- .gsub(/[\u200b\u200e\u200f]/, '')
158
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
150
+ native
151
+ .text
152
+ .delete(REMOVED_CHARACTERS)
153
+ .tr(SQUEEZED_SPACES, ' ')
154
+ .squeeze(' ')
159
155
  elsif native.element?
160
156
  text = native.children.map do |child|
161
157
  Capybara::RackTest::Node.new(driver, child).displayed_text(check_ancestor: false)
@@ -235,7 +231,14 @@ private
235
231
  end
236
232
  native.remove
237
233
  else
238
- native['value'] = value.to_s
234
+ value.to_s.tap do |set_value|
235
+ if set_value.end_with?("\n") && form&.css('input, textarea')&.count
236
+ native['value'] = set_value.to_s.chop
237
+ Capybara::RackTest::Form.new(driver, form).submit(self)
238
+ else
239
+ native['value'] = set_value
240
+ end
241
+ end
239
242
  end
240
243
  end
241
244
 
@@ -11,7 +11,7 @@ end
11
11
  Capybara.register_driver :selenium_headless do |app|
12
12
  version = Capybara::Selenium::Driver.load_selenium
13
13
  options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
14
- browser_options = ::Selenium::WebDriver::Firefox::Options.new.tap do |opts|
14
+ browser_options = Selenium::WebDriver::Firefox::Options.new.tap do |opts|
15
15
  opts.add_argument '-headless'
16
16
  end
17
17
  Capybara::Selenium::Driver.new(app, **{ :browser => :firefox, options_key => browser_options })
@@ -20,7 +20,7 @@ end
20
20
  Capybara.register_driver :selenium_chrome do |app|
21
21
  version = Capybara::Selenium::Driver.load_selenium
22
22
  options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
23
- browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
23
+ browser_options = Selenium::WebDriver::Chrome::Options.new.tap do |opts|
24
24
  # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
25
25
  opts.add_argument('--disable-site-isolation-trials')
26
26
  end
@@ -31,7 +31,7 @@ end
31
31
  Capybara.register_driver :selenium_chrome_headless do |app|
32
32
  version = Capybara::Selenium::Driver.load_selenium
33
33
  options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
34
- browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
34
+ browser_options = Selenium::WebDriver::Chrome::Options.new.tap do |opts|
35
35
  opts.add_argument('--headless')
36
36
  opts.add_argument('--disable-gpu') if Gem.win_platform?
37
37
  # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
@@ -5,12 +5,24 @@ Capybara.register_server :default do |app, port, _host|
5
5
  end
6
6
 
7
7
  Capybara.register_server :webrick do |app, port, host, **options|
8
- require 'rack/handler/webrick'
8
+ base_class = begin
9
+ require 'rack/handler/webrick'
10
+ Rack
11
+ rescue LoadError
12
+ # Rack 3 separated out the webrick handle - no way test currently in Capybaras automated
13
+ # tests due to Sinatra not yet supporting Rack 3 - experimental
14
+ require 'rackup/handler/webrick'
15
+ Rackup
16
+ end
9
17
  options = { Host: host, Port: port, AccessLog: [], Logger: WEBrick::Log.new(nil, 0) }.merge(options)
10
- Rack::Handler::WEBrick.run(app, **options)
18
+ base_class::Handler::WEBrick.run(app, **options)
11
19
  end
12
20
 
13
21
  Capybara.register_server :puma do |app, port, host, **options| # rubocop:disable Metrics/BlockLength
22
+ begin
23
+ require 'rackup'
24
+ rescue LoadError # rubocop:disable Lint/SuppressedException
25
+ end
14
26
  begin
15
27
  require 'rack/handler/puma'
16
28
  rescue LoadError
@@ -33,7 +45,7 @@ Capybara.register_server :puma do |app, port, host, **options| # rubocop:disable
33
45
  puma_ver = Gem::Version.new(Puma::Const::PUMA_VERSION)
34
46
  require_relative 'patches/puma_ssl' if Gem::Requirement.new('>=4.0.0', '< 4.1.0').satisfied_by?(puma_ver)
35
47
 
36
- logger = (defined?(::Puma::LogWriter) ? ::Puma::LogWriter : ::Puma::Events).then do |cls|
48
+ logger = (defined?(Puma::LogWriter) ? Puma::LogWriter : Puma::Events).then do |cls|
37
49
  conf.options[:Silent] ? cls.strings : cls.stdio
38
50
  end
39
51
  conf.options[:log_writer] = logger
@@ -44,7 +56,7 @@ Capybara.register_server :puma do |app, port, host, **options| # rubocop:disable
44
56
 
45
57
  Puma::Server.new(
46
58
  conf.app,
47
- defined?(::Puma::LogWriter) ? nil : logger,
59
+ defined?(Puma::LogWriter) ? nil : logger,
48
60
  conf.options
49
61
  ).tap do |s|
50
62
  s.binder.parse conf.options[:binds], (s.log_writer rescue s.events) # rubocop:disable Style/RescueModifier
@@ -36,7 +36,7 @@ if RUBY_ENGINE == 'jruby'
36
36
  end
37
37
  end
38
38
 
39
- if defined?(::RSpec::Matchers)
39
+ if defined?(RSpec::Matchers)
40
40
  module ::RSpec::Matchers
41
41
  def self.included(base)
42
42
  base.send(:include, ::Capybara::RSpecMatcherProxies) if base.include?(::Capybara::DSL)
@@ -76,7 +76,7 @@ else
76
76
  end
77
77
  end
78
78
 
79
- Capybara::DSL.prepend ::Capybara::DSLRSpecProxyInstaller
79
+ Capybara::DSL.prepend Capybara::DSLRSpecProxyInstaller
80
80
 
81
- ::RSpec::Matchers.prepend ::Capybara::RSpecMatcherProxyInstaller if defined?(::RSpec::Matchers)
81
+ RSpec::Matchers.prepend Capybara::RSpecMatcherProxyInstaller if defined?(RSpec::Matchers)
82
82
  end
@@ -47,14 +47,16 @@ module Capybara
47
47
  end
48
48
 
49
49
  class WrappedElementMatcher < Base
50
- def matches?(actual)
50
+ def matches?(actual, &filter_block)
51
+ @filter_block ||= filter_block
51
52
  element_matches?(wrap(actual))
52
53
  rescue Capybara::ExpectationNotMet => e
53
54
  @failure_message = e.message
54
55
  false
55
56
  end
56
57
 
57
- def does_not_match?(actual)
58
+ def does_not_match?(actual, &filter_block)
59
+ @filter_block ||= filter_block
58
60
  element_does_not_match?(wrap(actual))
59
61
  rescue Capybara::ExpectationNotMet => e
60
62
  @failure_message_when_negated = e.message
@@ -86,12 +88,12 @@ module Capybara
86
88
  @matcher = matcher
87
89
  end
88
90
 
89
- def matches?(actual)
90
- @matcher.does_not_match?(actual)
91
+ def matches?(actual, &filter_block)
92
+ @matcher.does_not_match?(actual, &filter_block)
91
93
  end
92
94
 
93
- def does_not_match?(actual)
94
- @matcher.matches?(actual)
95
+ def does_not_match?(actual, &filter_block)
96
+ @matcher.matches?(actual, &filter_block)
95
97
  end
96
98
 
97
99
  def description
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if defined?(::RSpec::Expectations::Version)
3
+ if defined?(RSpec::Expectations::Version)
4
4
  module Capybara
5
5
  module RSpecMatchers
6
6
  module Matchers
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Capybara.add_selector(:link, locator_type: [String, Symbol]) do
4
- xpath do |locator, href: true, alt: nil, title: nil, **|
4
+ xpath do |locator, href: true, alt: nil, title: nil, target: nil, **|
5
5
  xpath = XPath.descendant(:a)
6
6
  xpath = builder(xpath).add_attribute_conditions(href: href) unless href == false
7
7
 
@@ -25,6 +25,7 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
25
25
 
26
26
  xpath = xpath[find_by_attr(:title, title)]
27
27
  xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
28
+ xpath = xpath[find_by_attr(:target, target)] if target
28
29
 
29
30
  xpath
30
31
  end
@@ -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', '< 4.8.0')
16
16
 
17
17
  attr_reader :app, :options
18
18
 
@@ -315,16 +315,16 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
315
315
  ].tap do |errors|
316
316
  unless selenium_4?
317
317
  ::Selenium::WebDriver.logger.suppress_deprecations do
318
- errors.concat [
318
+ errors.push(
319
319
  ::Selenium::WebDriver::Error::UnhandledError,
320
320
  ::Selenium::WebDriver::Error::ElementNotVisibleError,
321
321
  ::Selenium::WebDriver::Error::InvalidElementStateError,
322
322
  ::Selenium::WebDriver::Error::ElementNotSelectableError
323
- ]
323
+ )
324
324
  end
325
325
  end
326
326
  if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
327
- errors.concat([::Selenium::WebDriver::Error::DetachedShadowRootError])
327
+ errors.push(::Selenium::WebDriver::Error::DetachedShadowRootError)
328
328
  end
329
329
  end
330
330
  end
@@ -103,9 +103,13 @@ private
103
103
  end
104
104
 
105
105
  def execute_cdp(cmd, params = {})
106
- args = { cmd: cmd, params: params }
107
- result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
108
- result['value']
106
+ if browser.respond_to? :execute_cdp
107
+ browser.execute_cdp(cmd, **params)
108
+ else
109
+ args = { cmd: cmd, params: params }
110
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/ms/cdp/execute", args)
111
+ result['value']
112
+ end
109
113
  end
110
114
 
111
115
  def build_node(native_node, initial_cache = {})
@@ -115,7 +119,7 @@ private
115
119
  def edgedriver_version
116
120
  @edgedriver_version ||= begin
117
121
  caps = browser.capabilities
118
- caps['chrome']&.fetch('chromedriverVersion', nil).to_f
122
+ caps['msedge']&.fetch('msedgedriverVersion', nil).to_f
119
123
  end
120
124
  end
121
125
  end
@@ -166,6 +166,9 @@ class Capybara::Selenium::Node
166
166
  opts[key + 'Key'] = true;
167
167
  }
168
168
 
169
+ var dragEnterEvent = new DragEvent('dragenter', opts);
170
+ target.dispatchEvent(dragEnterEvent);
171
+
169
172
  // fire 2 dragover events to simulate dragging with a direction
170
173
  var entryPoint = pointOnRect(sourceCenter, targetRect)
171
174
  var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
@@ -4,8 +4,10 @@
4
4
 
5
5
  require 'capybara/selenium/extensions/find'
6
6
  require 'capybara/selenium/extensions/scroll'
7
+ require 'capybara/node/whitespace_normalizer'
7
8
 
8
9
  class Capybara::Selenium::Node < Capybara::Driver::Node
10
+ include Capybara::Node::WhitespaceNormalizer
9
11
  include Capybara::Selenium::Find
10
12
  include Capybara::Selenium::Scroll
11
13
 
@@ -17,11 +19,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
17
19
 
18
20
  def all_text
19
21
  text = driver.evaluate_script('arguments[0].textContent', self) || ''
20
- text.gsub(/[\u200b\u200e\u200f]/, '')
21
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
22
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
23
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
24
- .tr("\u00a0", ' ')
22
+ normalize_spacing(text)
25
23
  end
26
24
 
27
25
  def [](name)
@@ -221,7 +219,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
221
219
  end
222
220
 
223
221
  def shadow_root
224
- raise_error 'You must be using Selenium 4.1+ for shadow_root support' unless native.respond_to? :shadow_root
222
+ raise 'You must be using Selenium 4.1+ for shadow_root support' unless native.respond_to? :shadow_root
225
223
 
226
224
  root = native.shadow_root
227
225
  root && build_node(native.shadow_root)
@@ -237,7 +235,7 @@ protected
237
235
  end
238
236
 
239
237
  def scroll_to_center
240
- script = <<-'JS'
238
+ script = <<-JS
241
239
  try {
242
240
  arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
243
241
  } catch(e) {
@@ -520,7 +518,7 @@ private
520
518
  def attrs(*attr_names)
521
519
  return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
522
520
 
523
- driver.evaluate_script <<~'JS', self, attr_names.map(&:to_s)
521
+ driver.evaluate_script <<~JS, self, attr_names.map(&:to_s)
524
522
  (function(el, names){
525
523
  return names.map(function(name){
526
524
  return el[name]
@@ -571,7 +569,7 @@ private
571
569
  })(arguments[0], document)
572
570
  JS
573
571
 
574
- OBSCURED_OR_OFFSET_SCRIPT = <<~'JS'
572
+ OBSCURED_OR_OFFSET_SCRIPT = <<~JS
575
573
  (function(el, x, y) {
576
574
  var box = el.getBoundingClientRect();
577
575
  if (x == null) x = box.width/2;
@@ -588,7 +586,7 @@ private
588
586
  })(arguments[0], arguments[1], arguments[2])
589
587
  JS
590
588
 
591
- RAPID_APPEND_TEXT = <<~'JS'
589
+ RAPID_APPEND_TEXT = <<~JS
592
590
  (function(el, value) {
593
591
  value = el.value + value;
594
592
  if (el.maxLength && el.maxLength != -1){
@@ -106,7 +106,11 @@ private
106
106
 
107
107
  def file_errors
108
108
  @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do
109
- [::Selenium::WebDriver::Error::ExpectedError]
109
+ if defined? ::Selenium::WebDriver::Error::ExpectedError # Selenium < 4
110
+ [::Selenium::WebDriver::Error::ExpectedError]
111
+ else
112
+ []
113
+ end
110
114
  end
111
115
  end
112
116
 
@@ -38,7 +38,7 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
38
38
  html5_drop(*args)
39
39
  end
40
40
 
41
- def click(*)
41
+ def click(*, **)
42
42
  super
43
43
  rescue Selenium::WebDriver::Error::InvalidArgumentError => e
44
44
  tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
@@ -77,11 +77,33 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
77
77
  end
78
78
  end
79
79
 
80
+ def send_keys(*args)
81
+ args.chunk { |inp| inp.is_a?(String) && inp.match?(/\p{Emoji Presentation}/) }
82
+ .each do |contains_emoji, inputs|
83
+ if contains_emoji
84
+ inputs.join.grapheme_clusters.chunk { |gc| gc.match?(/\p{Emoji Presentation}/) }
85
+ .each do |emoji, clusters|
86
+ if emoji
87
+ driver.send(:execute_cdp, 'Input.insertText', text: clusters.join)
88
+ else
89
+ super(clusters.join)
90
+ end
91
+ end
92
+ else
93
+ super(*inputs)
94
+ end
95
+ end
96
+ end
97
+
80
98
  private
81
99
 
82
100
  def file_errors
83
101
  @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do
84
- [::Selenium::WebDriver::Error::ExpectedError]
102
+ if defined? ::Selenium::WebDriver::Error::ExpectedError # Selenium < 4
103
+ [::Selenium::WebDriver::Error::ExpectedError]
104
+ else
105
+ []
106
+ end
85
107
  end
86
108
  end
87
109
 
@@ -20,7 +20,7 @@ module ActionPauser
20
20
  private_constant :Pauser
21
21
  end
22
22
 
23
- if defined?(::Selenium::WebDriver::VERSION) && (::Selenium::WebDriver::VERSION.to_f < 4) &&
24
- defined?(::Selenium::WebDriver::ActionBuilder)
25
- ::Selenium::WebDriver::ActionBuilder.prepend(ActionPauser)
23
+ if defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f < 4) &&
24
+ defined?(Selenium::WebDriver::ActionBuilder)
25
+ Selenium::WebDriver::ActionBuilder.prepend(ActionPauser)
26
26
  end
@@ -15,4 +15,4 @@ private
15
15
  end
16
16
  end
17
17
 
18
- ::Selenium::WebDriver::Remote::Bridge.prepend CapybaraAtoms unless ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
18
+ Selenium::WebDriver::Remote::Bridge.prepend CapybaraAtoms unless ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
@@ -6,4 +6,4 @@ module PauseDurationFix
6
6
  end
7
7
  end
8
8
 
9
- ::Selenium::WebDriver::Interactions::Pause.prepend PauseDurationFix
9
+ Selenium::WebDriver::Interactions::Pause.prepend PauseDurationFix
@@ -18,8 +18,7 @@ module Capybara
18
18
  @app = app
19
19
  @disable_css_markup = format(DISABLE_CSS_MARKUP_TEMPLATE,
20
20
  selector: self.class.selector_for(Capybara.disable_animation))
21
- @disable_js_markup = format(DISABLE_JS_MARKUP_TEMPLATE,
22
- selector: self.class.selector_for(Capybara.disable_animation))
21
+ @disable_js_markup = +DISABLE_JS_MARKUP_TEMPLATE
23
22
  end
24
23
 
25
24
  def call(env)
@@ -40,7 +39,7 @@ module Capybara
40
39
  attr_reader :disable_css_markup, :disable_js_markup
41
40
 
42
41
  def html_content?(headers)
43
- /html/.match?(headers['Content-Type'])
42
+ /html/.match?(headers['Content-Type']) # rubocop:todo Performance/StringInclude
44
43
  end
45
44
 
46
45
  def insert_disable(html, nonces)
@@ -44,6 +44,10 @@ $(function() {
44
44
  $(this).after('<div class="log">DragOver with client position: ' + ev.clientX + ',' + ev.clientY)
45
45
  if ($(this).hasClass('drop')) { ev.preventDefault(); }
46
46
  });
47
+ $('#drop_html5, #drop_html5_scroll').on('dragenter', function(ev){
48
+ $(this).after('<div class="log">DragEnter')
49
+ if ($(this).hasClass('drop')) { ev.preventDefault(); }
50
+ });
47
51
  $('#drop_html5, #drop_html5_scroll').on('dragleave', function(ev){
48
52
  $(this).after('<div class="log">DragLeave with client position: ' + ev.clientX + ',' + ev.clientY)
49
53
  if ($(this).hasClass('drop')) { ev.preventDefault(); }
@@ -31,7 +31,7 @@ Capybara::SpecHelper.spec '#all' do
31
31
  it 'should accept an XPath instance', :exact_false do
32
32
  @session.visit('/form')
33
33
  @xpath = Capybara::Selector.new(:fillable_field, config: {}, format: :xpath).call('Name')
34
- expect(@xpath).to be_a(::XPath::Union)
34
+ expect(@xpath).to be_a(XPath::Union)
35
35
  @result = @session.all(@xpath).map(&:value)
36
36
  expect(@result).to include('Smith', 'John', 'John Smith')
37
37
  end
@@ -131,6 +131,17 @@ Capybara::SpecHelper.spec '#click_link' do
131
131
  end
132
132
  end
133
133
 
134
+ context 'with :target option given' do
135
+ it 'should find links with valid target' do
136
+ @session.click_link('labore', target: '_self')
137
+ expect(@session).to have_content('Bar')
138
+ end
139
+
140
+ it "should raise error if link wasn't found" do
141
+ expect { @session.click_link('labore', target: '_blank') }.to raise_error(Capybara::ElementNotFound, /Unable to find link "labore"/)
142
+ end
143
+ end
144
+
134
145
  it 'should follow relative links' do
135
146
  @session.visit('/')
136
147
  @session.click_link('Relative')