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.
- checksums.yaml +4 -4
- data/History.md +20 -0
- data/lib/capybara/node/actions.rb +4 -4
- data/lib/capybara/node/finders.rb +2 -0
- data/lib/capybara/node/whitespace_normalizer.rb +81 -0
- data/lib/capybara/rack_test/node.rb +18 -15
- data/lib/capybara/registrations/drivers.rb +3 -3
- data/lib/capybara/registrations/servers.rb +16 -4
- data/lib/capybara/rspec/matcher_proxies.rb +3 -3
- data/lib/capybara/rspec/matchers/base.rb +8 -6
- data/lib/capybara/rspec/matchers/compound.rb +1 -1
- data/lib/capybara/selector/definition/link.rb +2 -1
- data/lib/capybara/selenium/driver.rb +4 -4
- data/lib/capybara/selenium/driver_specializations/edge_driver.rb +8 -4
- data/lib/capybara/selenium/extensions/html5_drag.rb +3 -0
- data/lib/capybara/selenium/node.rb +8 -10
- data/lib/capybara/selenium/nodes/chrome_node.rb +5 -1
- data/lib/capybara/selenium/nodes/edge_node.rb +24 -2
- data/lib/capybara/selenium/patches/action_pauser.rb +3 -3
- data/lib/capybara/selenium/patches/atoms.rb +1 -1
- data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
- data/lib/capybara/server/animation_disabler.rb +2 -3
- data/lib/capybara/spec/public/test.js +4 -0
- data/lib/capybara/spec/session/all_spec.rb +1 -1
- data/lib/capybara/spec/session/click_link_spec.rb +11 -0
- data/lib/capybara/spec/session/find_link_spec.rb +10 -0
- data/lib/capybara/spec/session/find_spec.rb +1 -1
- data/lib/capybara/spec/session/first_spec.rb +1 -1
- data/lib/capybara/spec/session/frame/within_frame_spec.rb +2 -0
- data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
- data/lib/capybara/spec/session/has_any_selectors_spec.rb +2 -2
- data/lib/capybara/spec/session/has_current_path_spec.rb +1 -1
- data/lib/capybara/spec/session/has_link_spec.rb +5 -1
- data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
- data/lib/capybara/spec/session/matches_style_spec.rb +2 -0
- data/lib/capybara/spec/session/node_spec.rb +16 -0
- data/lib/capybara/spec/session/scroll_spec.rb +3 -1
- data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
- data/lib/capybara/spec/views/form.erb +4 -0
- data/lib/capybara/spec/views/with_html.erb +1 -1
- data/lib/capybara/version.rb +1 -1
- data/spec/css_builder_spec.rb +1 -1
- data/spec/css_splitter_spec.rb +1 -1
- data/spec/rack_test_spec.rb +2 -2
- data/spec/rspec/scenarios_spec.rb +1 -1
- data/spec/rspec_matchers_spec.rb +25 -0
- data/spec/sauce_spec_chrome.rb +1 -1
- data/spec/selenium_spec_chrome.rb +5 -6
- data/spec/selenium_spec_chrome_remote.rb +5 -7
- data/spec/selenium_spec_edge.rb +11 -7
- data/spec/selenium_spec_firefox.rb +10 -4
- data/spec/selenium_spec_firefox_remote.rb +16 -3
- data/spec/selenium_spec_ie.rb +1 -1
- data/spec/selenium_spec_safari.rb +1 -1
- data/spec/server_spec.rb +2 -2
- data/spec/shared_selenium_session.rb +2 -1
- data/spec/spec_helper.rb +33 -0
- data/spec/whitespace_normalizer_spec.rb +54 -0
- data/spec/xpath_builder_spec.rb +1 -1
- metadata +4 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ab39ff6da58cbd3edd267cec393f613a79bbabd1092e7929ad32f352ee46c914
         | 
| 4 | 
            +
              data.tar.gz: c61ad6d311f09fba5878e865133adfe5c8f16b65b3f1a184e2ef95655f7f7685
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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 = <<~ | 
| 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 = <<~ | 
| 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 = <<~ | 
| 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 = <<~ | 
| 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  
         | 
| 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 | 
| 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 | 
| 157 | 
            -
             | 
| 158 | 
            -
             | 
| 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 | 
            -
                   | 
| 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 =  | 
| 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 =  | 
| 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 =  | 
| 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 | 
            -
               | 
| 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 | 
            -
               | 
| 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?( | 
| 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?( | 
| 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?( | 
| 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  | 
| 79 | 
            +
              Capybara::DSL.prepend Capybara::DSLRSpecProxyInstaller
         | 
| 80 80 |  | 
| 81 | 
            -
               | 
| 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,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(' | 
| 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. | 
| 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. | 
| 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 | 
            -
                 | 
| 107 | 
            -
             | 
| 108 | 
            -
                 | 
| 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[' | 
| 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 | 
| 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 | 
            -
                 | 
| 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 = <<- | 
| 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 <<~ | 
| 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 = <<~ | 
| 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 = <<~ | 
| 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 | 
            -
                   | 
| 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 | 
            -
                   | 
| 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?( | 
| 24 | 
            -
               defined?( | 
| 25 | 
            -
               | 
| 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
         | 
| @@ -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 =  | 
| 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( | 
| 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')
         |