capybara 3.38.0 → 3.39.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 (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')