# frozen_string_literal: true

require 'timeout'
require 'nokogiri'
require 'xpath'
require 'forwardable'
require 'capybara/config'
require 'capybara/registration_container'

module Capybara
  class CapybaraError < StandardError; end
  class DriverNotFoundError < CapybaraError; end
  class FrozenInTime < CapybaraError; end
  class ElementNotFound < CapybaraError; end
  class ModalNotFound < CapybaraError; end
  class Ambiguous < ElementNotFound; end
  class ExpectationNotMet < ElementNotFound; end
  class FileNotFound < CapybaraError; end
  class UnselectNotAllowed < CapybaraError; end
  class NotSupportedByDriverError < CapybaraError; end
  class InfiniteRedirectError < CapybaraError; end
  class ScopeError < CapybaraError; end
  class WindowError < CapybaraError; end
  class ReadOnlyElementError < CapybaraError; end

  class << self
    extend Forwardable

    # DelegateCapybara global configurations
    # @!method app
    #   See {Capybara.configure}
    # @!method reuse_server
    #   See {Capybara.configure}
    # @!method threadsafe
    #   See {Capybara.configure}
    # @!method server
    #   See {Capybara.configure}
    # @!method default_driver
    #   See {Capybara.configure}
    # @!method javascript_driver
    #   See {Capybara.configure}
    # @!method use_html5_parsing
    #   See {Capybara.configure}
    Config::OPTIONS.each do |method|
      def_delegators :config, method, "#{method}="
    end

    # Delegate Capybara global configurations
    # @!method default_selector
    #   See {Capybara.configure}
    # @!method default_max_wait_time
    #   See {Capybara.configure}
    # @!method app_host
    #   See {Capybara.configure}
    # @!method always_include_port
    #   See {Capybara.configure}
    SessionConfig::OPTIONS.each do |method|
      def_delegators :config, method, "#{method}="
    end

    ##
    #
    # Configure Capybara to suit your needs.
    #
    #     Capybara.configure do |config|
    #       config.run_server = false
    #       config.app_host   = 'http://www.google.com'
    #     end
    #
    # #### Configurable options
    #
    # - **use_html5_parsing** (Boolean = `false`) - When Nokogiri >= 1.12.0 or `nokogumbo` is installed, whether HTML5 parsing will be used for HTML strings.
    # - **always_include_port** (Boolean = `false`) - Whether the Rack server's port should automatically be inserted into every visited URL
    #   unless another port is explicitly specified.
    # - **app_host** (String, `nil`) - The default host to use when giving a relative URL to visit, must be a valid URL e.g. `http://www.example.com`.
    # - **asset_host** (String = `nil`) - Where dynamic assets are hosted - will be prepended to relative asset locations if present.
    # - **automatic_label_click** (Boolean = `false`) - Whether {Capybara::Node::Element#choose Element#choose}, {Capybara::Node::Element#check Element#check},
    #   {Capybara::Node::Element#uncheck Element#uncheck} will attempt to click the associated `<label>` element if the checkbox/radio button are non-visible.
    # - **automatic_reload** (Boolean = `true`) - Whether to automatically reload elements as Capybara is waiting.
    # - **default_max_wait_time** (Numeric = `2`) - The maximum number of seconds to wait for asynchronous processes to finish.
    # - **default_normalize_ws** (Boolean = `false`) - Whether text predicates and matchers use normalize whitespace behavior.
    # - **default_retry_interval** (Numeric = `0.01`) - The number of seconds to delay the next check in asynchronous processes.
    # - **default_selector** (`:css`, `:xpath` = `:css`) - Methods which take a selector use the given type by default. See also {Capybara::Selector}.
    # - **default_set_options** (Hash = `{}`) - The default options passed to {Capybara::Node::Element#set Element#set}.
    # - **enable_aria_label** (Boolean = `false`) - Whether fields, links, and buttons will match against `aria-label` attribute.
    # - **enable_aria_role** (Boolean = `false`) - Selectors will check for relevant aria role (currently only `button`).
    # - **exact** (Boolean = `false`) - Whether locators are matched exactly or with substrings. Only affects selector conditions
    #   written using the `XPath#is` method.
    # - **exact_text** (Boolean = `false`) - Whether the text matchers and `:text` filter match exactly or on substrings.
    # - **ignore_hidden_elements** (Boolean = `true`) - Whether to ignore hidden elements on the page.
    # - **match** (`:one`, `:first`, `:prefer_exact`, `:smart` = `:smart`) - The matching strategy to find nodes.
    # - **predicates_wait** (Boolean = `true`) - Whether Capybara's predicate matchers use waiting behavior by default.
    # - **raise_server_errors** (Boolean = `true`) - Should errors raised in the server be raised in the tests?
    # - **reuse_server** (Boolean = `true`) - Whether to reuse the server thread between multiple sessions using the same app object.
    # - **run_server** (Boolean = `true`) - Whether to start a Rack server for the given Rack app.
    # - **save_path** (String = `Dir.pwd`) - Where to put pages saved through {Capybara::Session#save_page save_page}, {Capybara::Session#save_screenshot save_screenshot},
    #   {Capybara::Session#save_and_open_page save_and_open_page}, or {Capybara::Session#save_and_open_screenshot save_and_open_screenshot}.
    # - **server** (Symbol = `:default` (which uses puma)) - The name of the registered server to use when running the app under test.
    # - **server_port** (Integer) - The port Capybara will run the application server on, if not specified a random port will be used.
    # - **server_host** (String = "127.0.0.1") - The IP address Capybara will bind the application server to. If the test application is to be accessed from an external host, you will want to change this to "0.0.0.0" or to a more specific IP address that your test client can reach.
    # - **server_errors** (Array\<Class> = `[Exception]`) - Error classes that should be raised in the tests if they are raised in the server
    #   and {configure raise_server_errors} is `true`.
    # - **test_id** (Symbol, String, `nil` = `nil`) - Optional attribute to match locator against with built-in selectors along with id.
    # - **threadsafe** (Boolean = `false`) - Whether sessions can be configured individually.
    # - **w3c_click_offset** (Boolean = 'true') - Whether click offsets should be from element center (true) or top left (false)
    #
    # #### DSL Options
    #
    # When using `capybara/dsl`, the following options are also available:
    #
    # - **default_driver** (Symbol = `:rack_test`) - The name of the driver to use by default.
    # - **javascript_driver** (Symbol = `:selenium`) - The name of a driver to use for JavaScript enabled tests.
    #
    def configure
      yield config
    end

    ##
    #
    # Register a new driver for Capybara.
    #
    #     Capybara.register_driver :rack_test do |app|
    #       Capybara::RackTest::Driver.new(app)
    #     end
    #
    # @param [Symbol] name                    The name of the new driver
    # @yield [app]                            This block takes a rack app and returns a Capybara driver
    # @yieldparam [<Rack>] app                The rack application that this driver runs against. May be nil.
    # @yieldreturn [Capybara::Driver::Base]   A Capybara driver instance
    #
    def register_driver(name, &block)
      drivers.send(:register, name, block)
    end

    ##
    #
    # Register a new server for Capybara.
    #
    #     Capybara.register_server :webrick do |app, port, host|
    #       require 'rack/handler/webrick'
    #       Rack::Handler::WEBrick.run(app, ...)
    #     end
    #
    # @param [Symbol] name                    The name of the new driver
    # @yield [app, port, host]                This block takes a rack app and a port and returns a rack server listening on that port
    # @yieldparam [<Rack>] app                The rack application that this server will contain.
    # @yieldparam port                        The port number the server should listen on
    # @yieldparam host                        The host/ip to bind to
    #
    def register_server(name, &block)
      servers.send(:register, name.to_sym, block)
    end

    ##
    #
    # Add a new selector to Capybara. Selectors can be used by various methods in Capybara
    # to find certain elements on the page in a more convenient way. For example adding a
    # selector to find certain table rows might look like this:
    #
    #     Capybara.add_selector(:row) do
    #       xpath { |num| ".//tbody/tr[#{num}]" }
    #     end
    #
    # This makes it possible to use this selector in a variety of ways:
    #
    #     find(:row, 3)
    #     page.find('table#myTable').find(:row, 3).text
    #     page.find('table#myTable').has_selector?(:row, 3)
    #     within(:row, 3) { expect(page).to have_content('$100.000') }
    #
    # Here is another example:
    #
    #     Capybara.add_selector(:id) do
    #       xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
    #     end
    #
    # Note that this particular selector already ships with Capybara.
    #
    # @param [Symbol] name    The name of the selector to add
    # @yield                  A block executed in the context of the new {Capybara::Selector}
    #
    def add_selector(name, **options, &block)
      Capybara::Selector.add(name, **options, &block)
    end

    ##
    #
    # Modify a selector previously created by {Capybara.add_selector}.
    # For example, adding a new filter to the :button selector to filter based on
    # button style (a class) might look like this
    #
    #     Capybara.modify_selector(:button) do
    #       filter (:btn_style, valid_values: [:primary, :secondary]) { |node, style| node[:class].split.include? "btn-#{style}" }
    #     end
    #
    #
    # @param [Symbol] name    The name of the selector to modify
    # @yield                  A block executed in the context of the existing {Capybara::Selector}
    #
    def modify_selector(name, &block)
      Capybara::Selector.update(name, &block)
    end

    def drivers
      @drivers ||= RegistrationContainer.new
    end

    def servers
      @servers ||= RegistrationContainer.new
    end

    # Wraps the given string, which should contain an HTML document or fragment
    # in a {Capybara::Node::Simple} which exposes all {Capybara::Node::Matchers},
    # {Capybara::Node::Finders} and {Capybara::Node::DocumentMatchers}. This allows you to query
    # any string containing HTML in the exact same way you would query the current document in a Capybara
    # session.
    #
    # @example A single element
    #     node = Capybara.string('bar')
    #     anchor = node.first('a')
    #     anchor[:href] #=> 'foo'
    #     anchor.text #=> 'bar'
    #
    # @example Multiple elements
    #     node = Capybara.string <<-HTML
    #

    #
Home

    #
Projects

    #

    #     HTML
    #
    #     node.find('#projects').text # => 'Projects'
    #     node.has_selector?('li#home', text: 'Home')
    #     node.has_selector?('#projects')
    #     node.find('ul').find('li:first-child').text # => 'Home'
    #
    # @param [String] html              An html fragment or document
    # @return [Capybara::Node::Simple]   A node which has Capybara's finders and matchers
    #
    def string(html)
      Capybara::Node::Simple.new(html)
    end

    ##
    #
    # Runs Capybara's default server for the given application and port
    # under most circumstances you should not have to call this method
    # manually.
    #
    # @param [Rack Application] app    The rack application to run
    # @param [Integer] port              The port to run the application on
    #
    def run_default_server(app, port)
      servers[:puma].call(app, port, server_host)
    end

    ##
    #
    # @return [Symbol]    The name of the driver currently in use
    #
    def current_driver
      if threadsafe
        Thread.current.thread_variable_get :capybara_current_driver
      else
        @current_driver
      end || default_driver
    end
    alias_method :mode, :current_driver

    def current_driver=(name)
      if threadsafe
        Thread.current.thread_variable_set :capybara_current_driver, name
      else
        @current_driver = name
      end
    end

    ##
    #
    # Use the default driver as the current driver
    #
    def use_default_driver
      self.current_driver = nil
    end

    ##
    #
    # Yield a block using a specific driver
    #
    def using_driver(driver)
      previous_driver = Capybara.current_driver
      Capybara.current_driver = driver
      yield
    ensure
      self.current_driver = previous_driver
    end

    ##
    #
    # Yield a block using a specific wait time
    #
    def using_wait_time(seconds)
      previous_wait_time = Capybara.default_max_wait_time
      Capybara.default_max_wait_time = seconds
      yield
    ensure
      Capybara.default_max_wait_time = previous_wait_time
    end

    ##
    #
    # The current {Capybara::Session} based on what is set as {app} and {current_driver}.
    #
    # @return [Capybara::Session]     The currently used session
    #
    def current_session
      specified_session || session_pool["#{current_driver}:#{session_name}:#{app.object_id}"]
    end

    ##
    #
    # Reset sessions, cleaning out the pool of sessions. This will remove any session information such
    # as cookies.
    #
    def reset_sessions!
      # reset in reverse so sessions that started servers are reset last
      session_pool.reverse_each { |_mode, session| session.reset! }
    end
    alias_method :reset!, :reset_sessions!

    ##
    #
    # The current session name.
    #
    # @return [Symbol]    The name of the currently used session.
    #
    def session_name
      if threadsafe
        Thread.current.thread_variable_get(:capybara_session_name) ||
          Thread.current.thread_variable_set(:capybara_session_name, :default)
      else
        @session_name ||= :default
      end
    end

    def session_name=(name)
      if threadsafe
        Thread.current.thread_variable_set :capybara_session_name, name
      else
        @session_name = name
      end
    end

    ##
    #
    # Yield a block using a specific session name or {Capybara::Session} instance.
    #
    def using_session(name_or_session, &block)
      previous_session = current_session
      previous_session_info = {
        specified_session: specified_session,
        session_name: session_name,
        current_driver: current_driver,
        app: app
      }
      self.specified_session = self.session_name = nil
      if name_or_session.is_a? Capybara::Session
        self.specified_session = name_or_session
      else
        self.session_name = name_or_session
      end

      if block.arity.zero?
        yield
      else
        yield current_session, previous_session
      end
    ensure
      self.session_name, self.specified_session = previous_session_info.values_at(:session_name, :specified_session)
      self.current_driver, self.app = previous_session_info.values_at(:current_driver, :app) if threadsafe
    end

    ##
    #
    # Parse raw html into a document using Nokogiri, and adjust textarea contents as defined by the spec.
    #
    # @param [String] html              The raw html
    # @return [Nokogiri::HTML::Document]      HTML document
    #
    def HTML(html) # rubocop:disable Naming/MethodName
      # Nokogiri >= 1.12.0 or Nokogumbo installed and allowed for use
      html_parser, using_html5 = if defined?(Nokogiri::HTML5) && Capybara.use_html5_parsing
        [Nokogiri::HTML5, true]
      else
        [defined?(Nokogiri::HTML4) ? Nokogiri::HTML4 : Nokogiri::HTML, false]
      end

      html_parser.parse(html).tap do |document|
        document.xpath('//template').each do |template|
          # template elements content is not part of the document
          template.inner_html = ''
        end
        document.xpath('//textarea').each do |textarea|
          # The Nokogiri HTML5 parser already returns spec compliant contents
          textarea['_capybara_raw_value'] = using_html5 ? textarea.content : textarea.content.delete_prefix("\n")
        end
      end
    end

    def session_options
      config.session_options
    end

  private

    def config
      @config ||= Capybara::Config.new
    end

    def session_pool
      @session_pool ||= Hash.new do |hash, name|
        hash[name] = Capybara::Session.new(current_driver, app)
      end
    end

    def specified_session
      if threadsafe
        Thread.current.thread_variable_get :capybara_specified_session
      else
        @specified_session ||= nil
      end
    end

    def specified_session=(session)
      if threadsafe
        Thread.current.thread_variable_set :capybara_specified_session, session
      else
        @specified_session = session
      end
    end
  end

  self.default_driver = nil
  self.current_driver = nil
  self.server_host = nil

  module Driver; end
  module RackTest; end
  module Selenium; end

  require 'capybara/helpers'
  require 'capybara/session'
  require 'capybara/window'
  require 'capybara/server'
  require 'capybara/selector'
  require 'capybara/result'
  require 'capybara/version'

  require 'capybara/queries/base_query'
  require 'capybara/queries/selector_query'
  require 'capybara/queries/text_query'
  require 'capybara/queries/title_query'
  require 'capybara/queries/current_path_query'
  require 'capybara/queries/match_query'
  require 'capybara/queries/ancestor_query'
  require 'capybara/queries/sibling_query'
  require 'capybara/queries/style_query'
  require 'capybara/queries/active_element_query'

  require 'capybara/node/finders'
  require 'capybara/node/matchers'
  require 'capybara/node/actions'
  require 'capybara/node/document_matchers'
  require 'capybara/node/simple'
  require 'capybara/node/base'
  require 'capybara/node/element'
  require 'capybara/node/document'

  require 'capybara/driver/base'
  require 'capybara/driver/node'

  require 'capybara/rack_test/driver'
  require 'capybara/rack_test/node'
  require 'capybara/rack_test/form'
  require 'capybara/rack_test/browser'
  require 'capybara/rack_test/css_handlers'

  require 'capybara/selenium/node'
  require 'capybara/selenium/driver'
end

require 'capybara/registrations/servers'
require 'capybara/registrations/drivers'

Capybara.configure do |config|
  config.always_include_port = false
  config.run_server = true
  config.server = :default
  config.default_selector = :css
  config.default_max_wait_time = 2
  config.default_retry_interval = 0.01
  config.ignore_hidden_elements = true
  config.default_host = 'http://www.example.com'
  config.automatic_reload = true
  config.match = :smart
  config.exact = false
  config.exact_text = false
  config.raise_server_errors = true
  config.server_errors = [Exception]
  config.visible_text_only = false
  config.automatic_label_click = false
  config.enable_aria_label = false
  config.enable_aria_role = false
  config.reuse_server = true
  config.default_set_options = {}
  config.test_id = nil
  config.predicates_wait = true
  config.default_normalize_ws = false
  config.use_html5_parsing = false
  config.w3c_click_offset = true
end


def HTML(html) # rubocop:disable Naming/MethodName
      # Nokogiri >= 1.12.0 or Nokogumbo installed and allowed for use
      html_parser, using_html5 = if defined?(Nokogiri::HTML5) && Capybara.use_html5_parsing
        [Nokogiri::HTML5, true]
      else
        [defined?(Nokogiri::HTML4) ? Nokogiri::HTML4 : Nokogiri::HTML, false]
      end

      html_parser.parse(html).tap do |document|
        document.xpath('//template').each do |template|
          # template elements content is not part of the document
          template.inner_html = ''
        end
        document.xpath('//textarea').each do |textarea|
          # The Nokogiri HTML5 parser already returns spec compliant contents
          textarea['_capybara_raw_value'] = using_html5 ? textarea.content : textarea.content.delete_prefix("\n")
        end
      end
    end


# frozen_string_literal: true

require 'forwardable'
require 'capybara/session/config'

module Capybara
  class Config
    extend Forwardable

    OPTIONS = %i[
      app reuse_server threadsafe server default_driver javascript_driver use_html5_parsing allow_gumbo
    ].freeze

    attr_accessor :app, :use_html5_parsing
    attr_reader :reuse_server, :threadsafe, :session_options # rubocop:disable Style/BisectedAttrAccessor
    attr_writer :default_driver, :javascript_driver

    SessionConfig::OPTIONS.each do |method|
      def_delegators :session_options, method, "#{method}="
    end

    def initialize
      @session_options = Capybara::SessionConfig.new
      @javascript_driver = nil
    end

    attr_writer :reuse_server # rubocop:disable Style/BisectedAttrAccessor

    def threadsafe=(bool)
      if (bool != threadsafe) && Session.instance_created?
        raise 'Threadsafe setting cannot be changed once a session is created'
      end

      @threadsafe = bool
    end

    ##
    #
    # Return the proc that Capybara will call to run the Rack application.
    # The block returned receives a rack app, port, and host/ip and should run a Rack handler
    # By default, Capybara will try to use puma.
    #
    attr_reader :server

    ##
    #
    # Set the server to use.
    #
    #     Capybara.server = :webrick
    #     Capybara.server = :puma, { Silent: true }
    #
    # @overload server=(name)
    #   @param [Symbol] name     Name of the server type to use
    # @overload server=([name, options])
    #   @param [Symbol] name Name of the server type to use
    #   @param [Hash] options Options to pass to the server block
    # @see register_server
    #
    def server=(name)
      name, options = *name if name.is_a? Array
      @server = if name.respond_to? :call
        name
      elsif options
        proc { |app, port, host| Capybara.servers[name.to_sym].call(app, port, host, **options) }
      else
        Capybara.servers[name.to_sym]
      end
    end

    ##
    #
    # @return [Symbol]    The name of the driver to use by default
    #
    def default_driver
      @default_driver || :rack_test
    end

    ##
    #
    # @return [Symbol]    The name of the driver used when JavaScript is needed
    #
    def javascript_driver
      @javascript_driver || :selenium
    end

    def deprecate(method, alternate_method, once: false)
      @deprecation_notified ||= {}
      unless once && @deprecation_notified[method]
        Capybara::Helpers.warn "DEPRECATED: ##{method} is deprecated, please use ##{alternate_method} instead: #{Capybara::Helpers.filter_backtrace(caller)}"
      end
      @deprecation_notified[method] = true
    end

    def allow_gumbo=(val)
      deprecate('allow_gumbo=', 'use_html5_parsing=')
      self.use_html5_parsing = val
    end

    def allow_gumbo
      deprecate('allow_gumbo', 'use_html5_parsing')
      use_html5_parsing
    end
  end
end


# frozen_string_literal: true

require 'forwardable'

module Capybara
  ##
  # A {Capybara::Result} represents a collection of {Capybara::Node::Element} on the page. It is possible to interact with this
  # collection similar to an Array because it implements Enumerable and offers the following Array methods through delegation:
  #
  # * \[\]
  # * each()
  # * at()
  # * size()
  # * count()
  # * length()
  # * first()
  # * last()
  # * empty?()
  # * values_at()
  # * sample()
  #
  # @see Capybara::Node::Element
  #
  class Result
    include Enumerable
    extend Forwardable

    def initialize(elements, query)
      @elements = elements
      @result_cache = []
      @filter_errors = []
      @results_enum = lazy_select_elements { |node| query.matches_filters?(node, @filter_errors) }
      @query = query
      @allow_reload = false
    end

    def_delegators :full_results, :size, :length, :last, :values_at, :inspect, :sample, :to_ary

    alias index find_index

    def each(&block)
      return enum_for(:each) unless block

      @result_cache.each(&block)
      loop do
        next_result = @results_enum.next
        add_to_cache(next_result)
        yield next_result
      end
      self
    end

    def [](*args)
      idx, length = args
      max_idx = case idx
      when Integer
        if idx.negative?
          nil
        else
          length.nil? ? idx : idx + length - 1
        end
      when Range
        # idx.max is broken with beginless ranges
        # idx.end && idx.max # endless range will have end == nil
        max = idx.end
        max = nil if max&.negative?
        max -= 1 if max && idx.exclude_end?
        max
      end

      if max_idx.nil?
        full_results[*args]
      else
        load_up_to(max_idx + 1)
        @result_cache[*args]
      end
    end
    alias :at :[]

    def empty?
      !any?
    end

    def compare_count
      return 0 unless @query

      count, min, max, between = @query.options.values_at(:count, :minimum, :maximum, :between)

      # Only check filters for as many elements as necessary to determine result
      if count && (count = Integer(count))
        return load_up_to(count + 1) <=> count
      end

      return -1 if min && (min = Integer(min)) && (load_up_to(min) < min)

      return 1 if max && (max = Integer(max)) && (load_up_to(max + 1) > max)

      if between
        min, max = (between.begin && between.min) || 1, between.end
        max -= 1 if max && between.exclude_end?

        size = load_up_to(max ? max + 1 : min)
        return size <=> min unless between.include?(size)
      end

      0
    end

    def matches_count?
      compare_count.zero?
    end

    def failure_message
      message = @query.failure_message
      if count.zero?
        message << ' but there were no matches'
      else
        message << ", found #{count} #{Capybara::Helpers.declension('match', 'matches', count)}: " \
                << full_results.map { |r| r.text.inspect }.join(', ')
      end
      unless rest.empty?
        elements = rest.map { |el| el.text rescue '<<ERROR>>' }.map(&:inspect).join(', ') # rubocop:disable Style/RescueModifier
        message << '. Also found ' << elements << ', which matched the selector but not all filters. '
        message << @filter_errors.join('. ') if (rest.size == 1) && count.zero?
      end
      message
    end

    def negative_failure_message
      failure_message.sub(/(to find)/, 'not \1')
    end

    def unfiltered_size
      @elements.length
    end

    ##
    # @api private
    #
    def allow_reload!
      @allow_reload = true
      self
    end

  private

    def add_to_cache(elem)
      elem.allow_reload!(@result_cache.size) if @allow_reload
      @result_cache << elem
    end

    def load_up_to(num)
      loop do
        break if @result_cache.size >= num

        add_to_cache(@results_enum.next)
      end
      @result_cache.size
    end

    def full_results
      loop { @result_cache << @results_enum.next }
      @result_cache
    end

    def rest
      @rest ||= @elements - full_results
    end

    if RUBY_PLATFORM == 'java'
      # JRuby < 9.2.8.0 has an issue with lazy enumerators which
      # causes a concurrency issue with network requests here
      # https://github.com/jruby/jruby/issues/4212
      # while JRuby >= 9.2.8.0 leaks threads when using lazy enumerators
      # https://github.com/teamcapybara/capybara/issues/2349
      # so disable the use and JRuby users will need to pay a performance penalty
      def lazy_select_elements(&block)
        @elements.select(&block).to_enum # non-lazy evaluation
      end
    else
      def lazy_select_elements(&block)
        @elements.lazy.select(&block)
      end
    end
  end
end


def [](*args)
      idx, length = args
      max_idx = case idx
      when Integer
        if idx.negative?
          nil
        else
          length.nil? ? idx : idx + length - 1
        end
      when Range
        # idx.max is broken with beginless ranges
        # idx.end && idx.max # endless range will have end == nil
        max = idx.end
        max = nil if max&.negative?
        max -= 1 if max && idx.exclude_end?
        max
      end

      if max_idx.nil?
        full_results[*args]
      else
        load_up_to(max_idx + 1)
        @result_cache[*args]
      end
    end


def compare_count
      return 0 unless @query

      count, min, max, between = @query.options.values_at(:count, :minimum, :maximum, :between)

      # Only check filters for as many elements as necessary to determine result
      if count && (count = Integer(count))
        return load_up_to(count + 1) <=> count
      end

      return -1 if min && (min = Integer(min)) && (load_up_to(min) < min)

      return 1 if max && (max = Integer(max)) && (load_up_to(max + 1) > max)

      if between
        min, max = (between.begin && between.min) || 1, between.end
        max -= 1 if max && between.exclude_end?

        size = load_up_to(max ? max + 1 : min)
        return size <=> min unless between.include?(size)
      end

      0
    end


def failure_message
      message = @query.failure_message
      if count.zero?
        message << ' but there were no matches'
      else
        message << ", found #{count} #{Capybara::Helpers.declension('match', 'matches', count)}: " \
                << full_results.map { |r| r.text.inspect }.join(', ')
      end
      unless rest.empty?
        elements = rest.map { |el| el.text rescue '<<ERROR>>' }.map(&:inspect).join(', ') # rubocop:disable Style/RescueModifier
        message << '. Also found ' << elements << ', which matched the selector but not all filters. '
        message << @filter_errors.join('. ') if (rest.size == 1) && count.zero?
      end
      message
    end


# frozen_string_literal: true

require 'uri'
require 'net/http'
require 'rack'
require 'capybara/server/middleware'
require 'capybara/server/animation_disabler'
require 'capybara/server/checker'

module Capybara
  # @api private
  class Server
    class << self
      def ports
        @ports ||= {}
      end
    end

    attr_reader :app, :port, :host

    def initialize(app,
                   *deprecated_options,
                   port: Capybara.server_port,
                   host: Capybara.server_host,
                   reportable_errors: Capybara.server_errors,
                   extra_middleware: [])
      unless deprecated_options.empty?
        warn 'Positional arguments, other than the application, to Server#new are deprecated, please use keyword arguments'
      end
      @app = app
      @extra_middleware = extra_middleware
      @server_thread = nil # suppress warnings
      @host = deprecated_options[1] || host
      @reportable_errors = deprecated_options[2] || reportable_errors
      @port = deprecated_options[0] || port
      @port ||= Capybara::Server.ports[port_key]
      @port ||= find_available_port(host)
      @checker = Checker.new(@host, @port)
    end

    def reset_error!
      middleware.clear_error
    end

    def error
      middleware.error
    end

    def using_ssl?
      @checker.ssl?
    end

    def responsive?
      return false if @server_thread&.join(0)

      res = @checker.request { |http| http.get('/__identify__') }

      res.body == app.object_id.to_s if res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPRedirection)
    rescue SystemCallError, Net::ReadTimeout, OpenSSL::SSL::SSLError
      false
    end

    def wait_for_pending_requests
      timer = Capybara::Helpers.timer(expire_in: 60)
      while pending_requests?
        raise "Requests did not finish in 60 seconds: #{middleware.pending_requests}" if timer.expired?

        sleep 0.01
      end
    end

    def boot
      unless responsive?
        Capybara::Server.ports[port_key] = port

        @server_thread = Thread.new do
          Capybara.server.call(middleware, port, host)
        end

        timer = Capybara::Helpers.timer(expire_in: 60)
        until responsive?
          raise 'Rack application timed out during boot' if timer.expired?

          @server_thread.join(0.1)
        end
      end

      self
    end

    def base_url
      "http#{'s' if using_ssl?}://#{host}:#{port}"
    end

  private

    def middleware
      @middleware ||= Middleware.new(app, @reportable_errors, @extra_middleware)
    end

    def port_key
      Capybara.reuse_server ? app.object_id : middleware.object_id
    end

    def pending_requests?
      middleware.pending_requests?
    end

    def find_available_port(host)
      server = TCPServer.new(host, 0)
      port = server.addr[1]
      server.close

      # Workaround issue where some platforms (mac, ???) when passed a host
      # of '0.0.0.0' will return a port that is only available on one of the
      # ip addresses that resolves to, but the next binding to that port requires
      # that port to be available on all ips
      server = TCPServer.new(host, port)
      port
    rescue Errno::EADDRINUSE
      retry
    ensure
      server&.close
    end
  end
end


def boot
      unless responsive?
        Capybara::Server.ports[port_key] = port

        @server_thread = Thread.new do
          Capybara.server.call(middleware, port, host)
        end

        timer = Capybara::Helpers.timer(expire_in: 60)
        until responsive?
          raise 'Rack application timed out during boot' if timer.expired?

          @server_thread.join(0.1)
        end
      end

      self
    end


def find_available_port(host)
      server = TCPServer.new(host, 0)
      port = server.addr[1]
      server.close

      # Workaround issue where some platforms (mac, ???) when passed a host
      # of '0.0.0.0' will return a port that is only available on one of the
      # ip addresses that resolves to, but the next binding to that port requires
      # that port to be available on all ips
      server = TCPServer.new(host, port)
      port
    rescue Errno::EADDRINUSE
      retry
    ensure
      server&.close
    end


# frozen_string_literal: true

require 'capybara/session/matchers'
require 'addressable/uri'

module Capybara
  ##
  #
  # The {Session} class represents a single user's interaction with the system. The {Session} can use
  # any of the underlying drivers. A session can be initialized manually like this:
  #
  #     session = Capybara::Session.new(:culerity, MyRackApp)
  #
  # The application given as the second argument is optional. When running Capybara against an external
  # page, you might want to leave it out:
  #
  #     session = Capybara::Session.new(:culerity)
  #     session.visit('http://www.google.com')
  #
  # When {Capybara.configure threadsafe} is `true` the sessions options will be initially set to the
  # current values of the global options and a configuration block can be passed to the session initializer.
  # For available options see {Capybara::SessionConfig::OPTIONS}:
  #
  #     session = Capybara::Session.new(:driver, MyRackApp) do |config|
  #       config.app_host = "http://my_host.dev"
  #     end
  #
  # The {Session} provides a number of methods for controlling the navigation of the page, such as {#visit},
  # {#current_path}, and so on. It also delegates a number of methods to a {Capybara::Document}, representing
  # the current HTML document. This allows interaction:
  #
  #     session.fill_in('q', with: 'Capybara')
  #     session.click_button('Search')
  #     expect(session).to have_content('Capybara')
  #
  # When using `capybara/dsl`, the {Session} is initialized automatically for you.
  #
  class Session
    include Capybara::SessionMatchers

    NODE_METHODS = %i[
      all first attach_file text check choose scroll_to scroll_by
      click double_click right_click
      click_link_or_button click_button click_link
      fill_in find find_all find_button find_by_id find_field find_link
      has_content? has_text? has_css? has_no_content? has_no_text?
      has_no_css? has_no_xpath? has_xpath? select uncheck
      has_element? has_no_element?
      has_link? has_no_link? has_button? has_no_button? has_field?
      has_no_field? has_checked_field? has_unchecked_field?
      has_no_table? has_table? unselect has_select? has_no_select?
      has_selector? has_no_selector? click_on has_no_checked_field?
      has_no_unchecked_field? query assert_selector assert_no_selector
      assert_all_of_selectors assert_none_of_selectors assert_any_of_selectors
      refute_selector assert_text assert_no_text
    ].freeze
    # @api private
    DOCUMENT_METHODS = %i[
      title assert_title assert_no_title has_title? has_no_title?
    ].freeze
    SESSION_METHODS = %i[
      body html source current_url current_host current_path
      execute_script evaluate_script evaluate_async_script visit refresh go_back go_forward send_keys
      within within_element within_fieldset within_table within_frame switch_to_frame
      current_window windows open_new_window switch_to_window within_window window_opened_by
      save_page save_and_open_page save_screenshot
      save_and_open_screenshot reset_session! response_headers
      status_code current_scope
      assert_current_path assert_no_current_path has_current_path? has_no_current_path?
    ].freeze + DOCUMENT_METHODS
    MODAL_METHODS = %i[
      accept_alert accept_confirm dismiss_confirm accept_prompt dismiss_prompt
    ].freeze
    DSL_METHODS = NODE_METHODS + SESSION_METHODS + MODAL_METHODS

    attr_reader :mode, :app, :server
    attr_accessor :synchronized

    def initialize(mode, app = nil)
      if app && !app.respond_to?(:call)
        raise TypeError, 'The second parameter to Session::new should be a rack app if passed.'
      end

      @@instance_created = true # rubocop:disable Style/ClassVars
      @mode = mode
      @app = app
      if block_given?
        raise 'A configuration block is only accepted when Capybara.threadsafe == true' unless Capybara.threadsafe

        yield config
      end
      @server = if config.run_server && @app && driver.needs_server?
        server_options = { port: config.server_port, host: config.server_host, reportable_errors: config.server_errors }
        server_options[:extra_middleware] = [Capybara::Server::AnimationDisabler] if config.disable_animation
        Capybara::Server.new(@app, **server_options).boot
      end
      @touched = false
    end

    def driver
      @driver ||= begin
        unless Capybara.drivers[mode]
          other_drivers = Capybara.drivers.names.map(&:inspect)
          raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
        end
        driver = Capybara.drivers[mode].call(app)
        driver.session = self if driver.respond_to?(:session=)
        driver
      end
    end

    ##
    #
    # Reset the session (i.e. remove cookies and navigate to blank page).
    #
    # This method does not:
    #
    # * accept modal dialogs if they are present (Selenium driver now does, others may not)
    # * clear browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc.
    # * modify state of the driver/underlying browser in any other way
    #
    # as doing so will result in performance downsides and it's not needed to do everything from the list above for most apps.
    #
    # If you want to do anything from the list above on a general basis you can:
    #
    # * write RSpec/Cucumber/etc. after hook
    # * monkeypatch this method
    # * use Ruby's `prepend` method
    #
    def reset!
      if @touched
        driver.reset!
        @touched = false
        switch_to_frame(:top) rescue nil # rubocop:disable Style/RescueModifier
        @scopes = [nil]
      end
      @server&.wait_for_pending_requests
      raise_server_error!
    end
    alias_method :cleanup!, :reset!
    alias_method :reset_session!, :reset!

    ##
    #
    # Disconnect from the current driver. A new driver will be instantiated on the next interaction.
    #
    def quit
      @driver.quit if @driver.respond_to? :quit
      @document = @driver = nil
      @touched = false
      @server&.reset_error!
    end

    ##
    #
    # Raise errors encountered in the server.
    #
    def raise_server_error!
      return unless @server&.error

      # Force an explanation for the error being raised as the exception cause
      begin
        if config.raise_server_errors
          raise CapybaraError, 'Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true'
        end
      rescue CapybaraError => capy_error # rubocop:disable Naming/RescuedExceptionsVariableName
        raise @server.error, cause: capy_error
      ensure
        @server.reset_error!
      end
    end

    ##
    #
    # Returns a hash of response headers. Not supported by all drivers (e.g. Selenium).
    #
    # @return [Hash<String, String>] A hash of response headers.
    #
    def response_headers
      driver.response_headers
    end

    ##
    #
    # Returns the current HTTP status code as an integer. Not supported by all drivers (e.g. Selenium).
    #
    # @return [Integer] Current HTTP status code
    #
    def status_code
      driver.status_code
    end

    ##
    #
    # @return [String] A snapshot of the DOM of the current document, as it looks right now (potentially modified by JavaScript).
    #
    def html
      driver.html || ''
    end
    alias_method :body, :html
    alias_method :source, :html

    ##
    #
    # @return [String] Path of the current page, without any domain information
    #
    def current_path
      # Addressable parsing is more lenient than URI
      uri = ::Addressable::URI.parse(current_url)

      # Addressable doesn't support opaque URIs - we want nil here
      return nil if uri&.scheme == 'about'

      path = uri&.path
      path unless path&.empty?
    end

    ##
    #
    # @return [String] Host of the current page
    #
    def current_host
      uri = URI.parse(current_url)
      "#{uri.scheme}://#{uri.host}" if uri.host
    end

    ##
    #
    # @return [String] Fully qualified URL of the current page
    #
    def current_url
      driver.current_url
    end

    ##
    #
    # Navigate to the given URL. The URL can either be a relative URL or an absolute URL
    # The behaviour of either depends on the driver.
    #
    #     session.visit('/foo')
    #     session.visit('http://google.com')
    #
    # For drivers which can run against an external application, such as the selenium driver
    # giving an absolute URL will navigate to that page. This allows testing applications
    # running on remote servers. For these drivers, setting {Capybara.configure app_host} will make the
    # remote server the default. For example:
    #
    #     Capybara.app_host = 'http://google.com'
    #     session.visit('/') # visits the google homepage
    #
    # If {Capybara.configure always_include_port} is set to `true` and this session is running against
    # a rack application, then the port that the rack application is running on will automatically
    # be inserted into the URL. Supposing the app is running on port `4567`, doing something like:
    #
    #     visit("http://google.com/test")
    #
    # Will actually navigate to `http://google.com:4567/test`.
    #
    # @param [#to_s] visit_uri     The URL to navigate to. The parameter will be cast to a String.
    #
    def visit(visit_uri)
      raise_server_error!
      @touched = true

      visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
      base_uri = ::Addressable::URI.parse(config.app_host || server_url)

      if base_uri && [nil, 'http', 'https'].include?(visit_uri.scheme)
        if visit_uri.relative?
          visit_uri_parts = visit_uri.to_hash.compact

          # Useful to people deploying to a subdirectory
          # and/or single page apps where only the url fragment changes
          visit_uri_parts[:path] = base_uri.path + visit_uri.path

          visit_uri = base_uri.merge(visit_uri_parts)
        end
        adjust_server_port(visit_uri)
      end

      driver.visit(visit_uri.to_s)
    end

    ##
    #
    # Refresh the page.
    #
    def refresh
      raise_server_error!
      driver.refresh
    end

    ##
    #
    # Move back a single entry in the browser's history.
    #
    def go_back
      driver.go_back
    end

    ##
    #
    # Move forward a single entry in the browser's history.
    #
    def go_forward
      driver.go_forward
    end

    ##
    # @!method send_keys
    #   @see Capybara::Node::Element#send_keys
    #
    def send_keys(...)
      driver.send_keys(...)
    end

    ##
    #
    # Returns the element with focus.
    #
    # Not supported by Rack Test
    #
    def active_element
      Capybara::Queries::ActiveElementQuery.new.resolve_for(self)[0].tap(&:allow_reload!)
    end

    ##
    #
    # Executes the given block within the context of a node. {#within} takes the
    # same options as {Capybara::Node::Finders#find #find}, as well as a block. For the duration of the
    # block, any command to Capybara will be handled as though it were scoped
    # to the given element.
    #
    #     within(:xpath, './/div[@id="delivery-address"]') do
    #       fill_in('Street', with: '12 Main Street')
    #     end
    #
    # Just as with `#find`, if multiple elements match the selector given to
    # {#within}, an error will be raised, and just as with `#find`, this
    # behaviour can be controlled through the `:match` and `:exact` options.
    #
    # It is possible to omit the first parameter, in that case, the selector is
    # assumed to be of the type set in {Capybara.configure default_selector}.
    #
    #     within('div#delivery-address') do
    #       fill_in('Street', with: '12 Main Street')
    #     end
    #
    # Note that a lot of uses of {#within} can be replaced more succinctly with
    # chaining:
    #
    #     find('div#delivery-address').fill_in('Street', with: '12 Main Street')
    #
    # @overload within(*find_args)
    #   @param (see Capybara::Node::Finders#all)
    #
    # @overload within(a_node)
    #   @param [Capybara::Node::Base] a_node   The node in whose scope the block should be evaluated
    #
    # @raise  [Capybara::ElementNotFound]      If the scope can't be found before time expires
    #
    def within(*args, **kw_args)
      new_scope = args.first.respond_to?(:to_capybara_node) ? args.first.to_capybara_node : find(*args, **kw_args)
      begin
        scopes.push(new_scope)
        yield new_scope if block_given?
      ensure
        scopes.pop
      end
    end
    alias_method :within_element, :within

    ##
    #
    # Execute the given block within the a specific fieldset given the id or legend of that fieldset.
    #
    # @param [String] locator    Id or legend of the fieldset
    #
    def within_fieldset(locator, &block)
      within(:fieldset, locator, &block)
    end

    ##
    #
    # Execute the given block within the a specific table given the id or caption of that table.
    #
    # @param [String] locator    Id or caption of the table
    #
    def within_table(locator, &block)
      within(:table, locator, &block)
    end

    ##
    #
    # Switch to the given frame.
    #
    # If you use this method you are responsible for making sure you switch back to the parent frame when done in the frame changed to.
    # {#within_frame} is preferred over this method and should be used when possible.
    # May not be supported by all drivers.
    #
    # @overload switch_to_frame(element)
    #   @param [Capybara::Node::Element] element    iframe/frame element to switch to
    # @overload switch_to_frame(location)
    #   @param [Symbol] location relative location of the frame to switch to
    #                            * :parent - the parent frame
    #                            * :top - the top level document
    #
    def switch_to_frame(frame)
      case frame
      when Capybara::Node::Element
        driver.switch_to_frame(frame)
        scopes.push(:frame)
      when :parent
        if scopes.last != :frame
          raise Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's " \
                                      '`within` block.'
        end
        scopes.pop
        driver.switch_to_frame(:parent)
      when :top
        idx = scopes.index(:frame)
        top_level_scopes = [:frame, nil]
        if idx
          if scopes.slice(idx..).any? { |scope| !top_level_scopes.include?(scope) }
            raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's " \
                                        '`within` block.'
          end
          scopes.slice!(idx..)
          driver.switch_to_frame(:top)
        end
      else
        raise ArgumentError, 'You must provide a frame element, :parent, or :top when calling switch_to_frame'
      end
    end

    ##
    #
    # Execute the given block within the given iframe using given frame, frame name/id or index.
    # May not be supported by all drivers.
    #
    # @overload within_frame(element)
    #   @param [Capybara::Node::Element]  frame element
    # @overload within_frame([kind = :frame], locator, **options)
    #   @param [Symbol] kind      Optional selector type (:frame, :css, :xpath, etc.) - Defaults to :frame
    #   @param [String] locator   The locator for the given selector kind.  For :frame this is the name/id of a frame/iframe element
    # @overload within_frame(index)
    #   @param [Integer] index         index of a frame (0 based)
    def within_frame(*args, **kw_args)
      switch_to_frame(_find_frame(*args, **kw_args))
      begin
        yield if block_given?
      ensure
        switch_to_frame(:parent)
      end
    end

    ##
    # @return [Capybara::Window]   current window
    #
    def current_window
      Window.new(self, driver.current_window_handle)
    end

    ##
    # Get all opened windows.
    # The order of windows in returned array is not defined.
    # The driver may sort windows by their creation time but it's not required.
    #
    # @return [Array<Capybara::Window>]   an array of all windows
    #
    def windows
      driver.window_handles.map do |handle|
        Window.new(self, handle)
      end
    end

    ##
    # Open a new window.
    # The current window doesn't change as the result of this call.
    # It should be switched to explicitly.
    #
    # @return [Capybara::Window]   window that has been opened
    #
    def open_new_window(kind = :tab)
      window_opened_by do
        if driver.method(:open_new_window).arity.zero?
          driver.open_new_window
        else
          driver.open_new_window(kind)
        end
      end
    end

    ##
    # Switch to the given window.
    #
    # @overload switch_to_window(&block)
    #   Switches to the first window for which given block returns a value other than false or nil.
    #   If window that matches block can't be found, the window will be switched back and {Capybara::WindowError} will be raised.
    #   @example
    #     window = switch_to_window { title == 'Page title' }
    #   @raise [Capybara::WindowError]     if no window matches given block
    # @overload switch_to_window(window)
    #   @param window [Capybara::Window]   window that should be switched to
    #   @raise [Capybara::Driver::Base#no_such_window_error] if nonexistent (e.g. closed) window was passed
    #
    # @return [Capybara::Window]         window that has been switched to
    # @raise [Capybara::ScopeError]        if this method is invoked inside {#within} or
    #   {#within_frame} methods
    # @raise [ArgumentError]               if both or neither arguments were provided
    #
    def switch_to_window(window = nil, **options, &window_locator)
      raise ArgumentError, '`switch_to_window` can take either a block or a window, not both' if window && window_locator
      raise ArgumentError, '`switch_to_window`: either window or block should be provided' if !window && !window_locator

      unless scopes.last.nil?
        raise Capybara::ScopeError, '`switch_to_window` is not supposed to be invoked from ' \
                                    '`within` or `within_frame` blocks.'
      end

      _switch_to_window(window, **options, &window_locator)
    end

    ##
    # This method does the following:
    #
    # 1. Switches to the given window (it can be located by window instance/lambda/string).
    # 2. Executes the given block (within window located at previous step).
    # 3. Switches back (this step will be invoked even if an exception occurs at the second step).
    #
    # @overload within_window(window) { do_something }
    #   @param window [Capybara::Window]       instance of {Capybara::Window} class
    #     that will be switched to
    #   @raise [driver#no_such_window_error] if nonexistent (e.g. closed) window was passed
    # @overload within_window(proc_or_lambda) { do_something }
    #   @param lambda [Proc]                  First window for which lambda
    #     returns a value other than false or nil will be switched to.
    #   @example
    #     within_window(->{ page.title == 'Page title' }) { click_button 'Submit' }
    #   @raise [Capybara::WindowError]         if no window matching lambda was found
    #
    # @raise [Capybara::ScopeError]        if this method is invoked inside {#within_frame} method
    # @return                              value returned by the block
    #
    def within_window(window_or_proc)
      original = current_window
      scopes << nil
      begin
        case window_or_proc
        when Capybara::Window
          _switch_to_window(window_or_proc) unless original == window_or_proc
        when Proc
          _switch_to_window { window_or_proc.call }
        else
          raise ArgumentError, '`#within_window` requires a `Capybara::Window` instance or a lambda'
        end

        begin
          yield if block_given?
        ensure
          _switch_to_window(original) unless original == window_or_proc
        end
      ensure
        scopes.pop
      end
    end

    ##
    # Get the window that has been opened by the passed block.
    # It will wait for it to be opened (in the same way as other Capybara methods wait).
    # It's better to use this method than `windows.last`
    # {https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10 as order of windows isn't defined in some drivers}.
    #
    # @overload window_opened_by(**options, &block)
    #   @param options [Hash]
    #   @option options [Numeric] :wait  maximum wait time. Defaults to {Capybara.configure default_max_wait_time}
    #   @return [Capybara::Window]       the window that has been opened within a block
    #   @raise [Capybara::WindowError]   if block passed to window hasn't opened window
    #     or opened more than one window
    #
    def window_opened_by(**options)
      old_handles = driver.window_handles
      yield

      synchronize_windows(options) do
        opened_handles = (driver.window_handles - old_handles)
        if opened_handles.size != 1
          raise Capybara::WindowError, 'block passed to #window_opened_by ' \
                                       "opened #{opened_handles.size} windows instead of 1"
        end
        Window.new(self, opened_handles.first)
      end
    end

    ##
    #
    # Execute the given script, not returning a result. This is useful for scripts that return
    # complex objects, such as jQuery statements. {#execute_script} should be used over
    # {#evaluate_script} whenever possible.
    #
    # @param [String] script   A string of JavaScript to execute
    # @param args  Optional arguments that will be passed to the script. Driver support for this is optional and types of objects supported may differ between drivers
    #
    def execute_script(script, *args)
      @touched = true
      driver.execute_script(script, *driver_args(args))
    end

    ##
    #
    # Evaluate the given JavaScript and return the result. Be careful when using this with
    # scripts that return complex objects, such as jQuery statements. {#execute_script} might
    # be a better alternative.
    #
    # @param  [String] script   A string of JavaScript to evaluate
    # @param           args     Optional arguments that will be passed to the script
    # @return [Object]          The result of the evaluated JavaScript (may be driver specific)
    #
    def evaluate_script(script, *args)
      @touched = true
      result = driver.evaluate_script(script.strip, *driver_args(args))
      element_script_result(result)
    end

    ##
    #
    # Evaluate the given JavaScript and obtain the result from a callback function which will be passed as the last argument to the script.
    #
    # @param  [String] script   A string of JavaScript to evaluate
    # @param           args     Optional arguments that will be passed to the script
    # @return [Object]          The result of the evaluated JavaScript (may be driver specific)
    #
    def evaluate_async_script(script, *args)
      @touched = true
      result = driver.evaluate_async_script(script, *driver_args(args))
      element_script_result(result)
    end

    ##
    #
    # Execute the block, accepting a alert.
    #
    # @!macro modal_params
    #   Expects a block whose actions will trigger the display modal to appear.
    #   @example
    #     $0 do
    #       click_link('link that triggers appearance of system modal')
    #     end
    #   @overload $0(text, **options, &blk)
    #     @param text [String, Regexp]  Text or regex to match against the text in the modal. If not provided any modal is matched.
    #     @option options [Numeric] :wait  Maximum time to wait for the modal to appear after executing the block. Defaults to {Capybara.configure default_max_wait_time}.
    #     @yield Block whose actions will trigger the system modal
    #   @overload $0(**options, &blk)
    #     @option options [Numeric] :wait  Maximum time to wait for the modal to appear after executing the block. Defaults to {Capybara.configure default_max_wait_time}.
    #     @yield Block whose actions will trigger the system modal
    #   @return [String]  the message shown in the modal
    #   @raise [Capybara::ModalNotFound]  if modal dialog hasn't been found
    #
    def accept_alert(text = nil, **options, &blk)
      accept_modal(:alert, text, options, &blk)
    end

    ##
    #
    # Execute the block, accepting a confirm.
    #
    # @macro modal_params
    #
    def accept_confirm(text = nil, **options, &blk)
      accept_modal(:confirm, text, options, &blk)
    end

    ##
    #
    # Execute the block, dismissing a confirm.
    #
    # @macro modal_params
    #
    def dismiss_confirm(text = nil, **options, &blk)
      dismiss_modal(:confirm, text, options, &blk)
    end

    ##
    #
    # Execute the block, accepting a prompt, optionally responding to the prompt.
    #
    # @macro modal_params
    # @option options [String] :with   Response to provide to the prompt
    #
    def accept_prompt(text = nil, **options, &blk)
      accept_modal(:prompt, text, options, &blk)
    end

    ##
    #
    # Execute the block, dismissing a prompt.
    #
    # @macro modal_params
    #
    def dismiss_prompt(text = nil, **options, &blk)
      dismiss_modal(:prompt, text, options, &blk)
    end

    ##
    #
    # Save a snapshot of the page. If {Capybara.configure asset_host} is set it will inject `base` tag
    # pointing to {Capybara.configure asset_host}.
    #
    # If invoked without arguments it will save file to {Capybara.configure save_path}
    # and file will be given randomly generated filename. If invoked with a relative path
    # the path will be relative to {Capybara.configure save_path}.
    #
    # @param [String] path  the path to where it should be saved
    # @return [String]      the path to which the file was saved
    #
    def save_page(path = nil)
      prepare_path(path, 'html').tap do |p_path|
        File.write(p_path, Capybara::Helpers.inject_asset_host(body, host: config.asset_host), mode: 'wb')
      end
    end

    ##
    #
    # Save a snapshot of the page and open it in a browser for inspection.
    #
    # If invoked without arguments it will save file to {Capybara.configure save_path}
    # and file will be given randomly generated filename. If invoked with a relative path
    # the path will be relative to {Capybara.configure save_path}.
    #
    # @param [String] path  the path to where it should be saved
    #
    def save_and_open_page(path = nil)
      save_page(path).tap { |s_path| open_file(s_path) }
    end

    ##
    #
    # Save a screenshot of page.
    #
    # If invoked without arguments it will save file to {Capybara.configure save_path}
    # and file will be given randomly generated filename. If invoked with a relative path
    # the path will be relative to {Capybara.configure save_path}.
    #
    # @param [String] path    the path to where it should be saved
    # @param [Hash] options   a customizable set of options
    # @return [String]        the path to which the file was saved
    def save_screenshot(path = nil, **options)
      prepare_path(path, 'png').tap { |p_path| driver.save_screenshot(p_path, **options) }
    end

    ##
    #
    # Save a screenshot of the page and open it for inspection.
    #
    # If invoked without arguments it will save file to {Capybara.configure save_path}
    # and file will be given randomly generated filename. If invoked with a relative path
    # the path will be relative to {Capybara.configure save_path}.
    #
    # @param [String] path    the path to where it should be saved
    # @param [Hash] options   a customizable set of options
    #
    def save_and_open_screenshot(path = nil, **options)
      save_screenshot(path, **options).tap { |s_path| open_file(s_path) }
    end

    def document
      @document ||= Capybara::Node::Document.new(self, driver)
    end

    NODE_METHODS.each do |method|
      class_eval <<~METHOD, __FILE__, __LINE__ + 1
        def #{method}(...)
          @touched = true
          current_scope.#{method}(...)
        end
      METHOD
    end

    DOCUMENT_METHODS.each do |method|
      class_eval <<~METHOD, __FILE__, __LINE__ + 1
        def #{method}(...)
          document.#{method}(...)
        end
      METHOD
    end

    def inspect
      %(#<Capybara::Session>)
    end

    def current_scope
      scope = scopes.last
      [nil, :frame].include?(scope) ? document : scope
    end

    ##
    #
    # Yield a block using a specific maximum wait time.
    #
    def using_wait_time(seconds, &block)
      if Capybara.threadsafe
        begin
          previous_wait_time = config.default_max_wait_time
          config.default_max_wait_time = seconds
          yield
        ensure
          config.default_max_wait_time = previous_wait_time
        end
      else
        Capybara.using_wait_time(seconds, &block)
      end
    end

    ##
    #
    # Accepts a block to set the configuration options if {Capybara.configure threadsafe} is `true`. Note that some options only have an effect
    # if set at initialization time, so look at the configuration block that can be passed to the initializer too.
    #
    def configure
      raise 'Session configuration is only supported when Capybara.threadsafe == true' unless Capybara.threadsafe

      yield config
    end

    def self.instance_created?
      @@instance_created
    end

    def config
      @config ||= if Capybara.threadsafe
        Capybara.session_options.dup
      else
        Capybara::ReadOnlySessionConfig.new(Capybara.session_options)
      end
    end

    def server_url
      @server&.base_url
    end

  private

    @@instance_created = false # rubocop:disable Style/ClassVars

    def driver_args(args)
      args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg }
    end

    def accept_modal(type, text_or_options, options, &blk)
      driver.accept_modal(type, **modal_options(text_or_options, **options), &blk)
    end

    def dismiss_modal(type, text_or_options, options, &blk)
      driver.dismiss_modal(type, **modal_options(text_or_options, **options), &blk)
    end

    def modal_options(text = nil, **options)
      options[:text] ||= text unless text.nil?
      options[:wait] ||= config.default_max_wait_time
      options
    end

    def open_file(path)
      require 'launchy'
      Launchy.open(path)
    rescue LoadError
      warn "File saved to #{path}.\nPlease install the launchy gem to open the file automatically."
    end

    def prepare_path(path, extension)
      File.expand_path(path || default_fn(extension), config.save_path).tap do |p_path|
        FileUtils.mkdir_p(File.dirname(p_path))
      end
    end

    def default_fn(extension)
      timestamp = Time.new.strftime('%Y%m%d%H%M%S')
      "capybara-#{timestamp}#{rand(10**10)}.#{extension}"
    end

    def scopes
      @scopes ||= [nil]
    end

    def element_script_result(arg)
      case arg
      when Array
        arg.map { |subarg| element_script_result(subarg) }
      when Hash
        arg.transform_values! { |value| element_script_result(value) }
      when Capybara::Driver::Node
        Capybara::Node::Element.new(self, arg, nil, nil)
      else
        arg
      end
    end

    def adjust_server_port(uri)
      uri.port ||= @server.port if @server && config.always_include_port
    end

    def _find_frame(*args, **kw_args)
      case args[0]
      when Capybara::Node::Element
        args[0]
      when String, nil
        find(:frame, *args, **kw_args)
      when Symbol
        find(*args, **kw_args)
      when Integer
        idx = args[0]
        all(:frame, minimum: idx + 1)[idx]
      else
        raise TypeError
      end
    end

    def _switch_to_window(window = nil, **options, &window_locator)
      raise Capybara::ScopeError, 'Window cannot be switched inside a `within_frame` block' if scopes.include?(:frame)
      raise Capybara::ScopeError, 'Window cannot be switched inside a `within` block' unless scopes.last.nil?

      if window
        driver.switch_to_window(window.handle)
        window
      else
        synchronize_windows(options) do
          original_window_handle = driver.current_window_handle
          begin
            _switch_to_window_by_locator(&window_locator)
          rescue StandardError
            driver.switch_to_window(original_window_handle)
            raise
          end
        end
      end
    end

    def _switch_to_window_by_locator
      driver.window_handles.each do |handle|
        driver.switch_to_window handle
        return Window.new(self, handle) if yield
      end
      raise Capybara::WindowError, 'Could not find a window matching block/lambda'
    end

    def synchronize_windows(options, &block)
      wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
      document.synchronize(wait_time, errors: [Capybara::WindowError], &block)
    end
  end
end


def driver
      @driver ||= begin
        unless Capybara.drivers[mode]
          other_drivers = Capybara.drivers.names.map(&:inspect)
          raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
        end
        driver = Capybara.drivers[mode].call(app)
        driver.session = self if driver.respond_to?(:session=)
        driver
      end
    end


def reset!
      if @touched
        driver.reset!
        @touched = false
        switch_to_frame(:top) rescue nil # rubocop:disable Style/RescueModifier
        @scopes = [nil]
      end
      @server&.wait_for_pending_requests
      raise_server_error!
    end


def raise_server_error!
      return unless @server&.error

      # Force an explanation for the error being raised as the exception cause
      begin
        if config.raise_server_errors
          raise CapybaraError, 'Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true'
        end
      rescue CapybaraError => capy_error # rubocop:disable Naming/RescuedExceptionsVariableName
        raise @server.error, cause: capy_error
      ensure
        @server.reset_error!
      end
    end


def visit(visit_uri)
      raise_server_error!
      @touched = true

      visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
      base_uri = ::Addressable::URI.parse(config.app_host || server_url)

      if base_uri && [nil, 'http', 'https'].include?(visit_uri.scheme)
        if visit_uri.relative?
          visit_uri_parts = visit_uri.to_hash.compact

          # Useful to people deploying to a subdirectory
          # and/or single page apps where only the url fragment changes
          visit_uri_parts[:path] = base_uri.path + visit_uri.path

          visit_uri = base_uri.merge(visit_uri_parts)
        end
        adjust_server_port(visit_uri)
      end

      driver.visit(visit_uri.to_s)
    end


def switch_to_frame(frame)
      case frame
      when Capybara::Node::Element
        driver.switch_to_frame(frame)
        scopes.push(:frame)
      when :parent
        if scopes.last != :frame
          raise Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's " \
                                      '`within` block.'
        end
        scopes.pop
        driver.switch_to_frame(:parent)
      when :top
        idx = scopes.index(:frame)
        top_level_scopes = [:frame, nil]
        if idx
          if scopes.slice(idx..).any? { |scope| !top_level_scopes.include?(scope) }
            raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's " \
                                        '`within` block.'
          end
          scopes.slice!(idx..)
          driver.switch_to_frame(:top)
        end
      else
        raise ArgumentError, 'You must provide a frame element, :parent, or :top when calling switch_to_frame'
      end
    end


def within_window(window_or_proc)
      original = current_window
      scopes << nil
      begin
        case window_or_proc
        when Capybara::Window
          _switch_to_window(window_or_proc) unless original == window_or_proc
        when Proc
          _switch_to_window { window_or_proc.call }
        else
          raise ArgumentError, '`#within_window` requires a `Capybara::Window` instance or a lambda'
        end

        begin
          yield if block_given?
        ensure
          _switch_to_window(original) unless original == window_or_proc
        end
      ensure
        scopes.pop
      end
    end


def window_opened_by(**options)
      old_handles = driver.window_handles
      yield

      synchronize_windows(options) do
        opened_handles = (driver.window_handles - old_handles)
        if opened_handles.size != 1
          raise Capybara::WindowError, 'block passed to #window_opened_by ' \
                                       "opened #{opened_handles.size} windows instead of 1"
        end
        Window.new(self, opened_handles.first)
      end
    end


def using_wait_time(seconds, &block)
      if Capybara.threadsafe
        begin
          previous_wait_time = config.default_max_wait_time
          config.default_max_wait_time = seconds
          yield
        ensure
          config.default_max_wait_time = previous_wait_time
        end
      else
        Capybara.using_wait_time(seconds, &block)
      end
    end


def element_script_result(arg)
      case arg
      when Array
        arg.map { |subarg| element_script_result(subarg) }
      when Hash
        arg.transform_values! { |value| element_script_result(value) }
      when Capybara::Driver::Node
        Capybara::Node::Element.new(self, arg, nil, nil)
      else
        arg
      end
    end


# frozen_string_literal: true

module Capybara
  ##
  # The {Window} class represents a browser window.
  #
  # You can get an instance of the class by calling any of:
  #
  # * {Capybara::Session#windows}
  # * {Capybara::Session#current_window}
  # * {Capybara::Session#window_opened_by}
  # * {Capybara::Session#switch_to_window}
  #
  # Note that some drivers (e.g. Selenium) support getting size of/resizing/closing only
  # current window. So if you invoke such method for:
  #
  # * window that is current, Capybara will make 2 Selenium method invocations
  #   (get handle of current window + get size/resize/close).
  # * window that is not current, Capybara will make 4 Selenium method invocations
  #   (get handle of current window + switch to given handle + get size/resize/close + switch to original handle)
  #
  class Window
    # @return [String]   a string that uniquely identifies window within session
    attr_reader :handle

    # @return [Capybara::Session] session that this window belongs to
    attr_reader :session

    # @api private
    def initialize(session, handle)
      @session = session
      @driver = session.driver
      @handle = handle
    end

    ##
    # @return [Boolean] whether the window is not closed
    def exists?
      @driver.window_handles.include?(@handle)
    end

    ##
    # @return [Boolean] whether the window is closed
    def closed?
      !exists?
    end

    ##
    # @return [Boolean] whether this window is the window in which commands are being executed
    def current?
      @driver.current_window_handle == @handle
    rescue @driver.no_such_window_error
      false
    end

    ##
    # Close window.
    #
    # If this method was called for window that is current, then after calling this method
    # future invocations of other Capybara methods should raise
    # {Capybara::Driver::Base#no_such_window_error session.driver.no_such_window_error} until another window will be switched to.
    #
    # @!macro about_current
    #   If this method was called for window that is not current, then after calling this method
    #   current window should remain the same as it was before calling this method.
    #
    def close
      @driver.close_window(handle)
    end

    ##
    # Get window size.
    #
    # @macro about_current
    # @return [Array<(Integer, Integer)>] an array with width and height
    #
    def size
      @driver.window_size(handle)
    end

    ##
    # Resize window.
    #
    # @macro about_current
    # @param width [Integer]  the new window width in pixels
    # @param height [Integer]  the new window height in pixels
    #
    def resize_to(width, height)
      wait_for_stable_size { @driver.resize_window_to(handle, width, height) }
    end

    ##
    # Maximize window.
    #
    # If a particular driver (e.g. headless driver) doesn't have concept of maximizing it
    # may not support this method.
    #
    # @macro about_current
    #
    def maximize
      wait_for_stable_size { @driver.maximize_window(handle) }
    end

    ##
    # Fullscreen window.
    #
    # If a particular driver doesn't have concept of fullscreen it may not support this method.
    #
    # @macro about_current
    #
    def fullscreen
      @driver.fullscreen_window(handle)
    end

    def eql?(other)
      other.is_a?(self.class) && @session == other.session && @handle == other.handle
    end
    alias_method :==, :eql?

    def hash
      [@session, @handle].hash
    end

    def inspect
      "#<Window @handle=#{@handle.inspect}>"
    end

  private

    def wait_for_stable_size(seconds = session.config.default_max_wait_time)
      res = yield if block_given?
      timer = Capybara::Helpers.timer(expire_in: seconds)
      loop do
        prev_size = size
        sleep 0.025
        return res if prev_size == size
        break if timer.expired?
      end
      raise Capybara::WindowError, "Window size not stable within #{seconds} seconds."
    end
  end
end


# frozen_string_literal: true

module Capybara
  module Node
    module Actions
      # @!macro waiting_behavior
      #   If the driver is capable of executing JavaScript, this method will wait for a set amount of time
      #   and continuously retry finding the element until either the element is found or the time
      #   expires. The length of time this method will wait is controlled through {Capybara.configure default_max_wait_time}.
      #
      #   @option options [false, true, Numeric] wait
      #     Maximum time to wait for matching element to appear. Defaults to {Capybara.configure default_max_wait_time}.

      ##
      #
      # Finds a button or link and clicks it. See {#click_button} and
      # {#click_link} for what locator will match against for each type of element.
      #
      # @overload click_link_or_button([locator], **options)
      #   @macro waiting_behavior
      #   @param [String] locator      See {#click_button} and {#click_link}
      #
      # @return [Capybara::Node::Element]  The element clicked
      #
      def click_link_or_button(locator = nil, **options)
        find(:link_or_button, locator, **options).click
      end
      alias_method :click_on, :click_link_or_button

      ##
      #
      # Finds a link by id, {Capybara.configure test_id} attribute, text or title and clicks it. Also looks at image
      # alt text inside the link.
      #
      # @overload click_link([locator], **options)
      #   @macro waiting_behavior
      #   @param [String] locator         text, id, {Capybara.configure test_id} attribute, title or nested image's alt attribute
      #   @param [Hash] options           See {Capybara::Node::Finders#find_link}
      #
      # @return [Capybara::Node::Element]  The element clicked
      def click_link(locator = nil, **options)
        find(:link, locator, **options).click
      end

      ##
      #
      # Finds a button on the page and clicks it.
      # This can be any `<input>` element of type submit, reset, image, button or it can be a
      # `<button>` element. All buttons can be found by their id, name, {Capybara.configure test_id} attribute, value, or title. `<button>` elements can also be found
      # by their text content, and image `<input>` elements by their alt attribute.
      #
      # @overload click_button([locator], **options)
      #   @macro waiting_behavior
      #   @param [String] locator      Which button to find
      #   @param [Hash] options        See {Capybara::Node::Finders#find_button}
      # @return [Capybara::Node::Element]  The element clicked
      def click_button(locator = nil, **options)
        find(:button, locator, **options).click
      end

      ##
      #
      # Locate a text field or text area and fill it in with the given text.
      # The field can be found via its name, id, {Capybara.configure test_id} attribute, placeholder, or label text.
      # If no locator is provided this will operate on self or a descendant.
      #
      #     # will fill in a descendant fillable field with name, id, or label text matching 'Name'
      #     page.fill_in 'Name', with: 'Bob'
      #
      #     # will fill in `el` if it's a fillable field
      #     el.fill_in with: 'Tom'
      #
      #
      # @overload fill_in([locator], with:, **options)
      #   @param [String] locator                 Which field to fill in
      #   @param [Hash] options
      #   @param with: [String]                  The value to fill in
      #   @macro waiting_behavior
      #   @option options [String] currently_with The current value property of the field to fill in
      #   @option options [Boolean] multiple      Match fields that can have multiple values?
      #   @option options [String, Regexp] id             Match fields that match the id attribute
      #   @option options [String] name           Match fields that match the name attribute
      #   @option options [String] placeholder    Match fields that match the placeholder attribute
      #   @option options [String, Array<String>, Regexp] class    Match fields that match the class(es) provided
      #   @option options [Hash] fill_options     Driver specific options regarding how to fill fields (Defaults come from {Capybara.configure default_set_options})
      #
      # @return [Capybara::Node::Element]  The element filled in
      def fill_in(locator = nil, with:, currently_with: nil, fill_options: {}, **find_options)
        find_options[:with] = currently_with if currently_with
        find_options[:allow_self] = true if locator.nil?
        find(:fillable_field, locator, **find_options).set(with, **fill_options)
      end

      # @!macro label_click
      #   @option options [Boolean, Hash] allow_label_click
      #     Attempt to click the label to toggle state if element is non-visible. Defaults to {Capybara.configure automatic_label_click}.
      #     If set to a Hash it is passed as options to the `click` on the label

      ##
      #
      # Find a descendant radio button and mark it as checked. The radio button can be found
      # via name, id, {Capybara.configure test_id} attribute or label text. If no locator is
      # provided this will match against self or a descendant.
      #
      #     # will choose a descendant radio button with a name, id, or label text matching 'Male'
      #     page.choose('Male')
      #
      #     # will choose `el` if it's a radio button element
      #     el.choose()
      #
      # @overload choose([locator], **options)
      #   @param [String] locator           Which radio button to choose
      #
      #   @option options [String] option  Value of the radio_button to choose
      #   @option options [String, Regexp] id             Match fields that match the id attribute
      #   @option options [String] name           Match fields that match the name attribute
      #   @option options [String, Array<String>, Regexp] class    Match fields that match the class(es) provided
      #   @macro waiting_behavior
      #   @macro label_click
      #
      # @return [Capybara::Node::Element]  The element chosen or the label clicked
      def choose(locator = nil, **options)
        _check_with_label(:radio_button, true, locator, **options)
      end

      ##
      #
      # Find a descendant check box and mark it as checked. The check box can be found
      # via name, id, {Capybara.configure test_id} attribute, or label text. If no locator
      # is provided this will match against self or a descendant.
      #
      #     # will check a descendant checkbox with a name, id, or label text matching 'German'
      #     page.check('German')
      #
      #     # will check `el` if it's a checkbox element
      #     el.check()
      #
      #
      # @overload check([locator], **options)
      #   @param [String] locator           Which check box to check
      #
      #   @option options [String] option  Value of the checkbox to select
      #   @option options [String, Regexp] id       Match fields that match the id attribute
      #   @option options [String] name     Match fields that match the name attribute
      #   @option options [String, Array<String>, Regexp] class    Match fields that match the class(es) provided
      #   @macro label_click
      #   @macro waiting_behavior
      #
      # @return [Capybara::Node::Element]  The element checked or the label clicked
      def check(locator = nil, **options)
        _check_with_label(:checkbox, true, locator, **options)
      end

      ##
      #
      # Find a descendant check box and uncheck it. The check box can be found
      # via name, id, {Capybara.configure test_id} attribute, or label text. If
      # no locator is provided this will match against self or a descendant.
      #
      #     # will uncheck a descendant checkbox with a name, id, or label text matching 'German'
      #     page.uncheck('German')
      #
      #     # will uncheck `el` if it's a checkbox element
      #     el.uncheck()
      #
      #
      # @overload uncheck([locator], **options)
      #   @param [String] locator           Which check box to uncheck
      #
      #   @option options [String] option  Value of the checkbox to deselect
      #   @option options [String, Regexp] id       Match fields that match the id attribute
      #   @option options [String] name     Match fields that match the name attribute
      #   @option options [String, Array<String>, Regexp] class    Match fields that match the class(es) provided
      #   @macro label_click
      #   @macro waiting_behavior
      #
      # @return [Capybara::Node::Element]  The element unchecked or the label clicked
      def uncheck(locator = nil, **options)
        _check_with_label(:checkbox, false, locator, **options)
      end

      ##
      #
      # If `from` option is present, {#select} finds a select box, or text input with associated datalist,
      # on the page and selects a particular option from it.
      # Otherwise it finds an option inside current scope and selects it.
      # If the select box is a multiple select, {#select} can be called multiple times to select more than
      # one option.
      # The select box can be found via its name, id, {Capybara.configure test_id} attribute, or label text.
      # The option can be found by its text.
      #
      #     page.select 'March', from: 'Month'
      #
      # @overload select(value = nil, from: nil, **options)
      #   @macro waiting_behavior
      #
      #   @param value [String] Which option to select
      #   @param from [String]  The id, {Capybara.configure test_id} attribute, name or label of the select box
      #
      # @return [Capybara::Node::Element]  The option element selected
      def select(value = nil, from: nil, **options)
        raise ArgumentError, 'The :from option does not take an element' if from.is_a? Capybara::Node::Element

        el = from ? find_select_or_datalist_input(from, options) : self

        if el.respond_to?(:tag_name) && (el.tag_name == 'input')
          select_datalist_option(el, value)
        else
          el.find(:option, value, **options).select_option
        end
      end

      ##
      #
      # Find a select box on the page and unselect a particular option from it. If the select
      # box is a multiple select, {#unselect} can be called multiple times to unselect more than
      # one option. The select box can be found via its name, id, {Capybara.configure test_id} attribute,
      # or label text.
      #
      #     page.unselect 'March', from: 'Month'
      #
      # @overload unselect(value = nil, from: nil, **options)
      #   @macro waiting_behavior
      #
      #   @param value [String]     Which option to unselect
      #   @param from [String]      The id, {Capybara.configure test_id} attribute, name or label of the select box
      #
      #
      # @return [Capybara::Node::Element]  The option element unselected
      def unselect(value = nil, from: nil, **options)
        raise ArgumentError, 'The :from option does not take an element' if from.is_a? Capybara::Node::Element

        scope = from ? find(:select, from, **options) : self
        scope.find(:option, value, **options).unselect_option
      end

      ##
      #
      # Find a descendant file field on the page and attach a file given its path. There are two ways to use
      # {#attach_file}, in the first method the file field can be found via its name, id,
      # {Capybara.configure test_id} attribute, or label text. In the case of the file field being hidden for
      # styling reasons the `make_visible` option can be used to temporarily change the CSS of
      # the file field, attach the file, and then revert the CSS back to original. If no locator is
      # passed this will match self or a descendant.
      # The second method, which is currently in beta and may be changed/removed, involves passing a block
      # which performs whatever actions would trigger the file chooser to appear.
      #
      #     # will attach file to a descendant file input element that has a name, id, or label_text matching 'My File'
      #     page.attach_file('My File', '/path/to/file.png')
      #
      #     # will attach file to el if it's a file input element
      #     el.attach_file('/path/to/file.png')
      #
      #     # will attach file to whatever file input is triggered by the block
      #     page.attach_file('/path/to/file.png') do
      #       page.find('#upload_button').click
      #     end
      #
      # @overload attach_file([locator], paths, **options)
      #   @macro waiting_behavior
      #
      #   @param [String] locator                  Which field to attach the file to
      #   @param [String, Array<String>] paths     The path(s) of the file(s) that will be attached
      #
      #   @option options [Symbol] match
      #     The matching strategy to use (:one, :first, :prefer_exact, :smart). Defaults to {Capybara.configure match}.
      #   @option options [Boolean] exact
      #     Match the exact label name/contents or accept a partial match. Defaults to {Capybara.configure exact}.
      #   @option options [Boolean] multiple Match field which allows multiple file selection
      #   @option options [String, Regexp] id             Match fields that match the id attribute
      #   @option options [String] name           Match fields that match the name attribute
      #   @option options [String, Array<String>, Regexp] class    Match fields that match the class(es) provided
      #   @option options [true, Hash] make_visible
      #     A Hash of CSS styles to change before attempting to attach the file, if `true`, `{ opacity: 1, display: 'block', visibility: 'visible' }` is used (may not be supported by all drivers).
      # @overload attach_file(paths, &blk)
      #   @param [String, Array<String>] paths     The path(s) of the file(s) that will be attached
      #   @yield Block whose actions will trigger the system file chooser to be shown
      # @return [Capybara::Node::Element]  The file field element
      def attach_file(locator = nil, paths, make_visible: nil, **options) # rubocop:disable Style/OptionalArguments
        if locator && block_given?
          raise ArgumentError, '`#attach_file` does not support passing both a locator and a block'
        end

        Array(paths).each do |path|
          raise Capybara::FileNotFound, "cannot attach file, #{path} does not exist" unless File.exist?(path.to_s)
        end
        options[:allow_self] = true if locator.nil?

        if block_given?
          begin
            execute_script CAPTURE_FILE_ELEMENT_SCRIPT
            yield
            file_field = evaluate_script 'window._capybara_clicked_file_input'
            raise ArgumentError, "Capybara was unable to determine the file input you're attaching to" unless file_field
          rescue ::Capybara::NotSupportedByDriverError
            warn 'Block mode of `#attach_file` is not supported by the current driver - ignoring.'
          end
        end
        # Allow user to update the CSS style of the file input since they are so often hidden on a page
        if make_visible
          ff = file_field || find(:file_field, locator, **options.merge(visible: :all))
          while_visible(ff, make_visible) { |el| el.set(paths) }
        else
          (file_field || find(:file_field, locator, **options)).set(paths)
        end
      end

    private

      def find_select_or_datalist_input(from, options)
        synchronize(Capybara::Queries::BaseQuery.wait(options, session_options.default_max_wait_time)) do
          find(:select, from, **options)
        rescue Capybara::ElementNotFound => select_error # rubocop:disable Naming/RescuedExceptionsVariableName
          raise if %i[selected with_selected multiple].any? { |option| options.key?(option) }

          begin
            find(:datalist_input, from, **options)
          rescue Capybara::ElementNotFound => dlinput_error
            raise Capybara::ElementNotFound, "#{select_error.message} and #{dlinput_error.message}"
          end
        end
      end

      def select_datalist_option(input, value)
        datalist_options = input.evaluate_script(DATALIST_OPTIONS_SCRIPT)
        option = datalist_options.find { |opt| opt.values_at('value', 'label').include?(value) }
        raise ::Capybara::ElementNotFound, %(Unable to find datalist option "#{value}") unless option

        input.set(option['value'])
      rescue ::Capybara::NotSupportedByDriverError
        # Implement for drivers that don't support JS
        datalist = find(:xpath, XPath.descendant(:datalist)[XPath.attr(:id) == input[:list]], visible: false)
        option = datalist.find(:datalist_option, value, disabled: false)
        input.set(option.value)
      end

      def while_visible(element, visible_css)
        if visible_css == true
          visible_css = { opacity: 1, display: 'block', visibility: 'visible', width: 'auto', height: 'auto' }
        end
        _update_style(element, visible_css)
        unless element.visible?
          raise ExpectationNotMet, 'The style changes in :make_visible did not make the file input visible'
        end

        begin
          yield element
        ensure
          _reset_style(element)
        end
      end

      def _update_style(element, style)
        element.execute_script(UPDATE_STYLE_SCRIPT, style)
      rescue Capybara::NotSupportedByDriverError
        warn 'The :make_visible option is not supported by the current driver - ignoring'
      end

      def _reset_style(element)
        element.execute_script(RESET_STYLE_SCRIPT)
      rescue StandardError # rubocop:disable Lint/SuppressedException -- swallow extra errors
      end

      def _check_with_label(selector, checked, locator,
                            allow_label_click: session_options.automatic_label_click, **options)
        options[:allow_self] = true if locator.nil?
        synchronize(Capybara::Queries::BaseQuery.wait(options, session_options.default_max_wait_time)) do
          el = find(selector, locator, **options)
          el.set(checked)
        rescue StandardError => e
          raise unless allow_label_click && catch_error?(e)

          begin
            el ||= find(selector, locator, **options.merge(visible: :all))
            unless el.checked? == checked
              el.session
                .find(:label, for: el, visible: true, match: :first)
                .click(**(Hash.try_convert(allow_label_click) || {}))
            end
          rescue StandardError # swallow extra errors - raise original
            raise e
          end
        end
      end

      UPDATE_STYLE_SCRIPT = <<~JS
        this.capybara_style_cache = this.style.cssText;
        var css = arguments[0];
        for (var prop in css){
          if (css.hasOwnProperty(prop)) {
            this.style.setProperty(prop, css[prop], "important");
          }
        }
      JS

      RESET_STYLE_SCRIPT = <<~JS
        if (this.hasOwnProperty('capybara_style_cache')) {
          this.style.cssText = this.capybara_style_cache;
          delete this.capybara_style_cache;
        }
      JS

      DATALIST_OPTIONS_SCRIPT = <<~JS
        Array.prototype.slice.call((this.list||{}).options || []).
          filter(function(el){ return !el.disabled }).
          map(function(el){ return { "value": el.value, "label": el.label} })
      JS

      CAPTURE_FILE_ELEMENT_SCRIPT = <<~JS
        document.addEventListener('click', function file_catcher(e){
          if (e.target.matches("input[type='file']")) {
            window._capybara_clicked_file_input = e.target;
            this.removeEventListener('click', file_catcher);
            e.preventDefault();
          }
        }, {capture: true})
      JS
    end
  end
end


def find_select_or_datalist_input(from, options)
        synchronize(Capybara::Queries::BaseQuery.wait(options, session_options.default_max_wait_time)) do
          find(:select, from, **options)
        rescue Capybara::ElementNotFound => select_error # rubocop:disable Naming/RescuedExceptionsVariableName
          raise if %i[selected with_selected multiple].any? { |option| options.key?(option) }

          begin
            find(:datalist_input, from, **options)
          rescue Capybara::ElementNotFound => dlinput_error
            raise Capybara::ElementNotFound, "#{select_error.message} and #{dlinput_error.message}"
          end
        end
      end


def select_datalist_option(input, value)
        datalist_options = input.evaluate_script(DATALIST_OPTIONS_SCRIPT)
        option = datalist_options.find { |opt| opt.values_at('value', 'label').include?(value) }
        raise ::Capybara::ElementNotFound, %(Unable to find datalist option "#{value}") unless option

        input.set(option['value'])
      rescue ::Capybara::NotSupportedByDriverError
        # Implement for drivers that don't support JS
        datalist = find(:xpath, XPath.descendant(:datalist)[XPath.attr(:id) == input[:list]], visible: false)
        option = datalist.find(:datalist_option, value, disabled: false)
        input.set(option.value)
      end


def while_visible(element, visible_css)
        if visible_css == true
          visible_css = { opacity: 1, display: 'block', visibility: 'visible', width: 'auto', height: 'auto' }
        end
        _update_style(element, visible_css)
        unless element.visible?
          raise ExpectationNotMet, 'The style changes in :make_visible did not make the file input visible'
        end

        begin
          yield element
        ensure
          _reset_style(element)
        end
      end