# frozen_string_literal: true

module Capybara
  module Node
    ##
    #
    # A {Capybara::Node::Base} represents either an element on a page through the subclass
    # {Capybara::Node::Element} or a document through {Capybara::Node::Document}.
    #
    # Both types of Node share the same methods, used for interacting with the
    # elements on the page. These methods are divided into three categories,
    # finders, actions and matchers. These are found in the modules
    # {Capybara::Node::Finders}, {Capybara::Node::Actions} and {Capybara::Node::Matchers}
    # respectively.
    #
    # A {Capybara::Session} exposes all methods from {Capybara::Node::Document} directly:
    #
    #     session = Capybara::Session.new(:rack_test, my_app)
    #     session.visit('/')
    #     session.fill_in('Foo', with: 'Bar')    # from Capybara::Node::Actions
    #     bar = session.find('#bar')                # from Capybara::Node::Finders
    #     bar.select('Baz', from: 'Quox')        # from Capybara::Node::Actions
    #     session.has_css?('#foobar')               # from Capybara::Node::Matchers
    #
    class Base
      attr_reader :session, :base, :query_scope

      include Capybara::Node::Finders
      include Capybara::Node::Actions
      include Capybara::Node::Matchers

      def initialize(session, base)
        @session = session
        @base = base
      end

      # overridden in subclasses, e.g. Capybara::Node::Element
      def reload
        self
      end

      ##
      #
      # This method is Capybara's primary defence against asynchronicity
      # problems. It works by attempting to run a given block of code until it
      # succeeds. The exact behaviour of this method depends on a number of
      # factors. Basically there are certain exceptions which, when raised
      # from the block, instead of bubbling up, are caught, and the block is
      # re-run.
      #
      # Certain drivers, such as RackTest, have no support for asynchronous
      # processes, these drivers run the block, and any error raised bubbles up
      # immediately. This allows faster turn around in the case where an
      # expectation fails.
      #
      # Only exceptions that are {Capybara::ElementNotFound} or any subclass
      # thereof cause the block to be rerun. Drivers may specify additional
      # exceptions which also cause reruns. This usually occurs when a node is
      # manipulated which no longer exists on the page. For example, the
      # Selenium driver specifies
      # `Selenium::WebDriver::Error::ObsoleteElementError`.
      #
      # As long as any of these exceptions are thrown, the block is re-run,
      # until a certain amount of time passes. The amount of time defaults to
      # {Capybara.default_max_wait_time} and can be overridden through the `seconds`
      # argument. This time is compared with the system time to see how much
      # time has passed. On rubies/platforms which don't support access to a monotonic process clock
      # if the return value of `Time.now` is stubbed out, Capybara will raise `Capybara::FrozenInTime`.
      #
      # @param  [Integer] seconds  (current sessions default_max_wait_time) Maximum number of seconds to retry this block
      # @param  [Array<Exception>] errors (driver.invalid_element_errors +
      #   [Capybara::ElementNotFound]) exception types that cause the block to be rerun
      # @return [Object]                  The result of the given block
      # @raise  [Capybara::FrozenInTime]  If the return value of `Time.now` appears stuck
      #
      def synchronize(seconds = nil, errors: nil)
        return yield if session.synchronized

        seconds = session_options.default_max_wait_time if [nil, true].include? seconds
        interval = session_options.default_retry_interval
        session.synchronized = true
        timer = Capybara::Helpers.timer(expire_in: seconds)
        begin
          yield
        rescue StandardError => e
          session.raise_server_error!
          raise e unless catch_error?(e, errors)

          if driver.wait?
            raise e if timer.expired?

            sleep interval
            reload if session_options.automatic_reload
          else
            old_base = @base
            reload if session_options.automatic_reload
            raise e if old_base == @base
          end
          retry
        ensure
          session.synchronized = false
        end
      end

      # @api private
      def find_css(css, **options)
        if base.method(:find_css).arity == 1
          base.find_css(css)
        else
          base.find_css(css, **options)
        end
      end

      # @api private
      def find_xpath(xpath, **options)
        if base.method(:find_xpath).arity == 1
          base.find_xpath(xpath)
        else
          base.find_xpath(xpath, **options)
        end
      end

      # @api private
      def session_options
        session.config
      end

      def to_capybara_node
        self
      end

    protected

      def catch_error?(error, errors = nil)
        errors ||= (driver.invalid_element_errors + [Capybara::ElementNotFound])
        errors.any? { |type| error.is_a?(type) }
      end

      def driver
        session.driver
      end
    end
  end
end


# frozen_string_literal: true

module Capybara
  module Node
    ##
    #
    # A {Capybara::Node::Element} represents a single element on the page. It is possible
    # to interact with the contents of this element the same as with a document:
    #
    #     session = Capybara::Session.new(:rack_test, my_app)
    #
    #     bar = session.find('#bar')              # from Capybara::Node::Finders
    #     bar.select('Baz', from: 'Quox')      # from Capybara::Node::Actions
    #
    # {Capybara::Node::Element} also has access to HTML attributes and other properties of the
    # element:
    #
    #      bar.value
    #      bar.text
    #      bar[:title]
    #
    # @see Capybara::Node
    #
    class Element < Base
      def initialize(session, base, query_scope, query)
        super(session, base)
        @query_scope = query_scope
        @query = query
        @allow_reload = false
        @query_idx = nil
      end

      def allow_reload!(idx = nil)
        @query_idx = idx
        @allow_reload = true
      end

      ##
      #
      # @return [Object]    The native element from the driver, this allows access to driver specific methods
      #
      def native
        synchronize { base.native }
      end

      ##
      #
      # Retrieve the text of the element. If {Capybara.configure ignore_hidden_elements}
      # is `true`, which it is by default, then this will return only text
      # which is visible. The exact semantics of this may differ between
      # drivers, but generally any text within elements with `display:none` is
      # ignored. This behaviour can be overridden by passing `:all` to this
      # method.
      #
      # @param type [:all, :visible]  Whether to return only visible or all text
      # @return [String]              The text of the element
      #
      def text(type = nil, normalize_ws: false)
        type ||= :all unless session_options.ignore_hidden_elements || session_options.visible_text_only
        txt = synchronize { type == :all ? base.all_text : base.visible_text }
        normalize_ws ? txt.gsub(/[[:space:]]+/, ' ').strip : txt
      end

      ##
      #
      # Retrieve the given attribute.
      #
      #     element[:title] # => HTML title attribute
      #
      # @param  [Symbol] attribute     The attribute to retrieve
      # @return [String]               The value of the attribute
      #
      def [](attribute)
        synchronize { base[attribute] }
      end

      ##
      #
      # Retrieve the given CSS styles.
      #
      #     element.style('color', 'font-size') # => Computed values of CSS 'color' and 'font-size' styles
      #
      # @param [Array<String>] styles   Names of the desired CSS properties
      # @return [Hash]            Hash of the CSS property names to computed values
      #
      def style(*styles)
        styles = styles.flatten.map(&:to_s)
        raise ArgumentError, 'You must specify at least one CSS style' if styles.empty?

        begin
          synchronize { base.style(styles) }
        rescue NotImplementedError => e
          begin
            evaluate_script(STYLE_SCRIPT, *styles)
          rescue Capybara::NotSupportedByDriverError
            raise e
          end
        end
      end

      ##
      #
      # @return [String]    The value of the form element
      #
      def value
        synchronize { base.value }
      end

      ##
      #
      # Set the value of the form element to the given value.
      #
      # @param [String] value    The new value
      # @param [Hash] options    Driver specific options for how to set the value. Take default values from {Capybara.configure default_set_options}.
      #
      # @return [Capybara::Node::Element]  The element
      def set(value, **options)
        if ENV.fetch('CAPYBARA_THOROUGH', nil) && readonly?
          raise Capybara::ReadOnlyElementError, "Attempt to set readonly element with value: #{value}"
        end

        options = session_options.default_set_options.to_h.merge(options)
        synchronize { base.set(value, **options) }
        self
      end

      ##
      #
      # Select this node if it is an option element inside a select tag.
      #
      # @!macro action_waiting_behavior
      #   If the driver dynamic pages (JS) and the element is currently non-interactable, this method will
      #   continuously retry the action until either the element becomes interactable or the maximum
      #   wait time expires.
      #
      #   @param [false, Numeric] wait
      #     Maximum time to wait for the action to succeed. Defaults to {Capybara.configure default_max_wait_time}.
      # @return [Capybara::Node::Element]  The element
      def select_option(wait: nil)
        synchronize(wait) { base.select_option }
        self
      end

      ##
      #
      # Unselect this node if it is an option element inside a multiple select tag.
      #
      # @macro action_waiting_behavior
      # @return [Capybara::Node::Element]  The element
      def unselect_option(wait: nil)
        synchronize(wait) { base.unselect_option }
        self
      end

      ##
      #
      # Click the Element.
      #
      # @macro action_waiting_behavior
      # @!macro click_modifiers
      #   Both x: and y: must be specified if an offset is wanted, if not specified the click will occur at the middle of the element.
      #   @overload $0(*modifier_keys, wait: nil, **offset)
      #     @param *modifier_keys [:alt, :control, :meta, :shift] ([]) Keys to be held down when clicking
      #     @option options [Integer] x  X coordinate to offset the click location. If {Capybara.configure w3c_click_offset} is `true` the
      #       offset will be from the element center, otherwise it will be from the top left corner of the element
      #     @option options [Integer] y  Y coordinate to offset the click location. If {Capybara.configure w3c_click_offset} is `true` the
      #       offset will be from the element center, otherwise it will be from the top left corner of the element
      # @option options [Float] delay  Delay between the mouse down and mouse up events in seconds (0)
      # @return [Capybara::Node::Element]  The element
      def click(*keys, **options)
        perform_click_action(keys, **options) do |k, opts|
          base.click(k, **opts)
        end
      end

      ##
      #
      # Right Click the Element.
      #
      # @macro action_waiting_behavior
      # @macro click_modifiers
      # @option options [Float] delay  Delay between the mouse down and mouse up events in seconds (0)
      # @return [Capybara::Node::Element]  The element
      def right_click(*keys, **options)
        perform_click_action(keys, **options) do |k, opts|
          base.right_click(k, **opts)
        end
      end

      ##
      #
      # Double Click the Element.
      #
      # @macro action_waiting_behavior
      # @macro click_modifiers
      # @return [Capybara::Node::Element]  The element
      def double_click(*keys, **options)
        perform_click_action(keys, **options) do |k, opts|
          base.double_click(k, **opts)
        end
      end

      ##
      #
      # Send Keystrokes to the Element.
      #
      # @overload send_keys(keys, ...)
      #   @param keys [String, Symbol, Array<String,Symbol>]
      #
      # Examples:
      #
      #     element.send_keys "foo"                     #=> value: 'foo'
      #     element.send_keys "tet", :left, "s"         #=> value: 'test'
      #     element.send_keys [:control, 'a'], :space   #=> value: ' ' - assuming ctrl-a selects all contents
      #
      # Symbols supported for keys:
      # * :cancel
      # * :help
      # * :backspace
      # * :tab
      # * :clear
      # * :return
      # * :enter
      # * :shift
      # * :control
      # * :alt
      # * :pause
      # * :escape
      # * :space
      # * :page_up
      # * :page_down
      # * :end
      # * :home
      # * :left
      # * :up
      # * :right
      # * :down
      # * :insert
      # * :delete
      # * :semicolon
      # * :equals
      # * :numpad0
      # * :numpad1
      # * :numpad2
      # * :numpad3
      # * :numpad4
      # * :numpad5
      # * :numpad6
      # * :numpad7
      # * :numpad8
      # * :numpad9
      # * :multiply      - numeric keypad *
      # * :add           - numeric keypad +
      # * :separator     - numeric keypad 'separator' key ??
      # * :subtract      - numeric keypad -
      # * :decimal       - numeric keypad .
      # * :divide        - numeric keypad /
      # * :f1
      # * :f2
      # * :f3
      # * :f4
      # * :f5
      # * :f6
      # * :f7
      # * :f8
      # * :f9
      # * :f10
      # * :f11
      # * :f12
      # * :meta
      # * :command      - alias of :meta
      #
      # @return [Capybara::Node::Element]  The element
      def send_keys(*args)
        synchronize { base.send_keys(*args) }
        self
      end

      ##
      #
      # Hover on the Element.
      #
      # @return [Capybara::Node::Element]  The element
      def hover
        synchronize { base.hover }
        self
      end

      ##
      #
      # @return [String]      The tag name of the element
      #
      def tag_name
        # Element type is immutable so cache it
        @tag_name ||= initial_cache[:tag_name] || synchronize { base.tag_name }
      end

      ##
      #
      # Whether or not the element is visible. Not all drivers support CSS, so
      # the result may be inaccurate.
      #
      # @return [Boolean]     Whether the element is visible
      #
      def visible?
        synchronize { base.visible? }
      end

      ##
      #
      # Whether or not the element is currently in the viewport and it (or descendants)
      # would be considered clickable at the elements center point.
      #
      # @return [Boolean]     Whether the elements center is obscured.
      #
      def obscured?
        synchronize { base.obscured? }
      end

      ##
      #
      # Whether or not the element is checked.
      #
      # @return [Boolean]     Whether the element is checked
      #
      def checked?
        synchronize { base.checked? }
      end

      ##
      #
      # Whether or not the element is selected.
      #
      # @return [Boolean]     Whether the element is selected
      #
      def selected?
        synchronize { base.selected? }
      end

      ##
      #
      # Whether or not the element is disabled.
      #
      # @return [Boolean]     Whether the element is disabled
      #
      def disabled?
        synchronize { base.disabled? }
      end

      ##
      #
      # Whether or not the element is readonly.
      #
      # @return [Boolean]     Whether the element is readonly
      #
      def readonly?
        synchronize { base.readonly? }
      end

      ##
      #
      # Whether or not the element supports multiple results.
      #
      # @return [Boolean]     Whether the element supports multiple results.
      #
      def multiple?
        synchronize { base.multiple? }
      end

      ##
      #
      # An XPath expression describing where on the page the element can be found.
      #
      # @return [String]      An XPath expression
      #
      def path
        synchronize { base.path }
      end

      def rect
        synchronize { base.rect }
      end

      ##
      #
      # Trigger any event on the current element, for example mouseover or focus
      # events. Not supported with the Selenium driver, and SHOULDN'T BE USED IN TESTING unless you
      # fully understand why you're using it, that it can allow actions a user could never
      # perform, and that it may completely invalidate your test.
      #
      # @param [String] event       The name of the event to trigger
      #
      # @return [Capybara::Node::Element]  The element
      def trigger(event)
        synchronize { base.trigger(event) }
        self
      end

      ##
      #
      # Drag the element to the given other element.
      #
      #     source = page.find('#foo')
      #     target = page.find('#bar')
      #     source.drag_to(target)
      #
      # @param [Capybara::Node::Element] node     The element to drag to
      # @param [Hash] options  Driver specific options for dragging. May not be supported by all drivers.
      # @option options [Numeric] :delay   (0.05) When using Chrome/Firefox with Selenium and HTML5 dragging this is the number
      #                                    of seconds between each stage of the drag.
      # @option options [Boolean] :html5   When using Chrome/Firefox with Selenium enables to force the use of HTML5
      #                                    (true) or legacy (false) dragging. If not specified the driver will attempt to
      #                                    detect the correct method to use.
      # @option options [Array<Symbol>,Symbol] :drop_modifiers   Modifier keys which should be held while the dragged element is dropped.
      #
      #
      # @return [Capybara::Node::Element]  The dragged element
      def drag_to(node, **options)
        synchronize { base.drag_to(node.base, **options) }
        self
      end

      ##
      #
      # Drop items on the current element.
      #
      #     target = page.find('#foo')
      #     target.drop('/some/path/file.csv')
      #
      # @overload drop(path, ...)
      #   @param [String, #to_path] path Location of the file to drop on the element
      #
      # @overload drop(strings, ...)
      #   @param [Hash] strings A hash of type to data to be dropped - `{ "text/url" => "https://www.google.com" }`
      #
      # @return [Capybara::Node::Element]  The element
      def drop(*args)
        options = args.map { |arg| arg.respond_to?(:to_path) ? arg.to_path : arg }
        synchronize { base.drop(*options) }
        self
      end

      ##
      #
      # Scroll the page or element.
      #
      # @overload scroll_to(position, offset: [0,0])
      #   Scroll the page or element to its top, bottom or middle.
      #   @param [:top, :bottom, :center, :current] position
      #   @param [[Integer, Integer]] offset
      #
      # @overload scroll_to(element, align: :top)
      #   Scroll the page or current element until the given element is aligned at the top, bottom, or center of it.
      #   @param [Capybara::Node::Element] element   The element to be scrolled into view
      #   @param [:top, :bottom, :center] align Where to align the element being scrolled into view with relation to the current page/element if possible
      #
      # @overload scroll_to(x,y)
      #   @param [Integer] x    Horizontal scroll offset
      #   @param [Integer] y    Vertical scroll offset
      #
      # @return [Capybara::Node::Element]  The element
      def scroll_to(pos_or_el_or_x, y = nil, align: :top, offset: nil)
        case pos_or_el_or_x
        when Symbol
          synchronize { base.scroll_to(nil, pos_or_el_or_x) } unless pos_or_el_or_x == :current
        when Capybara::Node::Element
          synchronize { base.scroll_to(pos_or_el_or_x.base, align) }
        else
          synchronize { base.scroll_to(nil, nil, [pos_or_el_or_x, y]) }
        end
        synchronize { base.scroll_by(*offset) } unless offset.nil?
        self
      end

      ##
      #
      # Return the shadow_root for the current element
      #
      # @return [Capybara::Node::Element]  The shadow root

      def shadow_root
        root = synchronize { base.shadow_root }
        root && Capybara::Node::Element.new(session, root, nil, nil)
      end

      ##
      #
      # Execute the given JS in the context of the element 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 a result is not expected or needed. `this` in the script will refer to the element this is called on.
      #
      # @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)
        session.execute_script(<<~JS, self, *args)
          (function (){
            #{script}
          }).apply(arguments[0], Array.prototype.slice.call(arguments,1));
        JS
      end

      ##
      #
      # Evaluate the given JS in the context of the element 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. `this` in the script will refer to the element this is called on.
      #
      # @param  [String] script   A string of JavaScript to evaluate
      # @return [Object]          The result of the evaluated JavaScript (may be driver specific)
      #
      def evaluate_script(script, *args)
        session.evaluate_script(<<~JS, self, *args)
          (function(){
            return #{script.strip}
          }).apply(arguments[0], Array.prototype.slice.call(arguments,1));
        JS
      end

      ##
      #
      # Evaluate the given JavaScript in the context of the element and obtain the result from a
      # callback function which will be passed as the last argument to the script. `this` in the
      # script will refer to the element this is called on.
      #
      # @param  [String] script   A string of JavaScript to evaluate
      # @return [Object]          The result of the evaluated JavaScript (may be driver specific)
      #
      def evaluate_async_script(script, *args)
        session.evaluate_async_script(<<~JS, self, *args)
          (function (){
            #{script}
          }).apply(arguments[0], Array.prototype.slice.call(arguments,1));
        JS
      end

      ##
      #
      # Toggle the elements background color between white and black for a period of time.
      #
      # @return [Capybara::Node::Element]  The element
      def flash
        execute_script(<<~JS, 100)
          async function flash(el, delay){
            var old_bg = el.style.backgroundColor;
            var colors = ["black", "white"];
            for(var i=0; i<20; i++){
              el.style.backgroundColor = colors[i % colors.length];
              await new Promise(resolve => setTimeout(resolve, delay));
            }
            el.style.backgroundColor = old_bg;
          }
          flash(this, arguments[0]);
        JS

        self
      end

      # @api private
      def reload
        return self unless @allow_reload

        begin
          reloaded = @query.resolve_for(query_scope ? query_scope.reload : session)[@query_idx.to_i]
          @base = reloaded.base if reloaded
        rescue StandardError => e
          raise e unless catch_error?(e)
        end
        self
      end

      ##
      #
      # A human-readable representation of the element.
      #
      # @return [String]  A string representation
      def inspect
        %(#<Capybara::Node::Element tag="#{base.tag_name}" path="#{base.path}">)
      rescue NotSupportedByDriverError
        %(#<Capybara::Node::Element tag="#{base.tag_name}">)
      rescue *session.driver.invalid_element_errors
        %(Obsolete #<Capybara::Node::Element>)
      end

      # @api private
      def initial_cache
        base.respond_to?(:initial_cache) ? base.initial_cache : {}
      end

      STYLE_SCRIPT = <<~JS
        (function(){
          var s = window.getComputedStyle(this);
          var result = {};
          for (var i = arguments.length; i--; ) {
            var property_name = arguments[i];
            result[property_name] = s.getPropertyValue(property_name);
          }
          return result;
        }).apply(this, arguments)
      JS

    private

      def perform_click_action(keys, wait: nil, **options)
        raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ options[:x] ^ options[:y]

        options[:offset] ||= :center if session_options.w3c_click_offset
        synchronize(wait) { yield keys, options }
        self
      end
    end
  end
end


def scroll_to(pos_or_el_or_x, y = nil, align: :top, offset: nil)
        case pos_or_el_or_x
        when Symbol
          synchronize { base.scroll_to(nil, pos_or_el_or_x) } unless pos_or_el_or_x == :current
        when Capybara::Node::Element
          synchronize { base.scroll_to(pos_or_el_or_x.base, align) }
        else
          synchronize { base.scroll_to(nil, nil, [pos_or_el_or_x, y]) }
        end
        synchronize { base.scroll_by(*offset) } unless offset.nil?
        self
      end


def flash
        execute_script(<<~JS, 100)
          async function flash(el, delay){
            var old_bg = el.style.backgroundColor;
            var colors = ["black", "white"];
            for(var i=0; i<20; i++){
              el.style.backgroundColor = colors[i % colors.length];
              await new Promise(resolve => setTimeout(resolve, delay));
            }
            el.style.backgroundColor = old_bg;
          }
          flash(this, arguments[0]);
        JS

        self
      end


def reload
        return self unless @allow_reload

        begin
          reloaded = @query.resolve_for(query_scope ? query_scope.reload : session)[@query_idx.to_i]
          @base = reloaded.base if reloaded
        rescue StandardError => e
          raise e unless catch_error?(e)
        end
        self
      end


# frozen_string_literal: true

module Capybara
  module Node
    module Finders
      ##
      #
      # Find an {Capybara::Node::Element} based on the given arguments. {#find} will raise an error if the element
      # is not found.
      #
      #     page.find('#foo').find('.bar')
      #     page.find(:xpath, './/div[contains(., "bar")]')
      #     page.find('li', text: 'Quox').click_link('Delete')
      #
      # @param (see #all)
      #
      # @macro waiting_behavior
      #
      # @!macro system_filters
      #   @option options [String, Regexp] text      Only find elements which contain this text or match this regexp
      #   @option options [String, Regexp, String] exact_text
      #     When String the elements contained text must match exactly, when Boolean controls whether the `text` option must match exactly.
      #     Defaults to {Capybara.configure exact_text}.
      #   @option options [Boolean] normalize_ws
      #     Whether the `text`/`exact_text` options are compared against element text with whitespace normalized or as returned by the driver.
      #     Defaults to {Capybara.configure default_normalize_ws}.
      #   @option options [Boolean, Symbol] visible
      #     Only find elements with the specified visibility. Defaults to behavior indicated by {Capybara.configure ignore_hidden_elements}.
      #       * true - only finds visible elements.
      #       * false - finds invisible _and_ visible elements.
      #       * :all - same as false; finds visible and invisible elements.
      #       * :hidden - only finds invisible elements.
      #       * :visible - same as true; only finds visible elements.
      #   @option options [Boolean] obscured  Only find elements with the specified obscured state:
      #                                       * true - only find elements whose centerpoint is not in the viewport or is obscured by another non-descendant element.
      #                                       * false - only find elements whose centerpoint is in the viewport and is not obscured by other non-descendant elements.
      #   @option options [String, Regexp]  id           Only find elements with an id that matches the value passed
      #   @option options [String, Array<String>, Regexp] class  Only find elements with matching class/classes.
      #                                            * Absence of a class can be checked by prefixing the class name with `!`
      #                                            * If you need to check for existence of a class name that starts with `!` then prefix with `!!`
      #
      #                                                class:['a', '!b', '!!!c'] # limit to elements with class 'a' and '!c' but not class 'b'
      #
      #   @option options [String, Regexp, Hash] style  Only find elements with matching style. String and Regexp will be checked against text of the elements `style` attribute, while a Hash will be compared against the elements full style
      #   @option options [Boolean] exact            Control whether `is` expressions in the given XPath match exactly or partially. Defaults to {Capybara.configure exact}.
      #   @option options [Symbol] match        The matching strategy to use. Defaults to {Capybara.configure match}.
      #
      # @return [Capybara::Node::Element]      The found element
      # @raise  [Capybara::ElementNotFound]    If the element can't be found before time expires
      #
      def find(*args, **options, &optional_filter_block)
        options[:session_options] = session_options
        count_options = options.slice(*Capybara::Queries::BaseQuery::COUNT_KEYS)
        unless count_options.empty?
          Capybara::Helpers.warn(
            "'find' does not support count options (#{count_options}) ignoring. " \
            "Called from: #{Capybara::Helpers.filter_backtrace(caller)}"
          )
        end
        synced_resolve Capybara::Queries::SelectorQuery.new(*args, **options, &optional_filter_block)
      end

      ##
      #
      # Find an {Capybara::Node::Element} based on the given arguments that is also an ancestor of the element called on.
      # {#ancestor} will raise an error if the element is not found.
      #
      # {#ancestor} takes the same options as {#find}.
      #
      #     element.ancestor('#foo').find('.bar')
      #     element.ancestor(:xpath, './/div[contains(., "bar")]')
      #     element.ancestor('ul', text: 'Quox').click_link('Delete')
      #
      # @param (see #find)
      #
      # @macro waiting_behavior
      #
      # @return [Capybara::Node::Element]      The found element
      # @raise  [Capybara::ElementNotFound]    If the element can't be found before time expires
      #
      def ancestor(*args, **options, &optional_filter_block)
        options[:session_options] = session_options
        synced_resolve Capybara::Queries::AncestorQuery.new(*args, **options, &optional_filter_block)
      end

      ##
      #
      # Find an {Capybara::Node::Element} based on the given arguments that is also a sibling of the element called on.
      # {#sibling} will raise an error if the element is not found.
      #
      # {#sibling} takes the same options as {#find}.
      #
      #     element.sibling('#foo').find('.bar')
      #     element.sibling(:xpath, './/div[contains(., "bar")]')
      #     element.sibling('ul', text: 'Quox').click_link('Delete')
      #
      # @param (see #find)
      #
      # @macro waiting_behavior
      #
      # @return [Capybara::Node::Element]      The found element
      # @raise  [Capybara::ElementNotFound]    If the element can't be found before time expires
      #
      def sibling(*args, **options, &optional_filter_block)
        options[:session_options] = session_options
        synced_resolve Capybara::Queries::SiblingQuery.new(*args, **options, &optional_filter_block)
      end

      ##
      #
      # Find a form field on the page. The field can be found by its name, id or label text.
      #
      # @overload find_field([locator], **options)
      #   @param [String] locator             name, id, {Capybara.configure test_id} attribute, placeholder or text of associated label element
      #
      #   @macro waiting_behavior
      #
      #
      #   @option options [Boolean] checked       Match checked field?
      #   @option options [Boolean] unchecked     Match unchecked field?
      #   @option options [Boolean, Symbol] disabled (false)  Match disabled field?
      #                                                       * true - only finds a disabled field
      #                                                       * false - only finds an enabled field
      #                                                       * :all - finds either an enabled or disabled field
      #   @option options [Boolean] readonly      Match readonly field?
      #   @option options [String, Regexp] with   Value of field to match on
      #   @option options [String] type           Type of field to match on
      #   @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) passed
      # @return [Capybara::Node::Element]   The found element
      #
      def find_field(locator = nil, **options, &optional_filter_block)
        find(:field, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Find a link on the page. The link can be found by its id or text.
      #
      # @overload find_link([locator], **options)
      #   @param [String] locator            id, {Capybara.configure test_id} attribute, title, text, or alt of enclosed img element
      #
      #   @macro waiting_behavior
      #
      #   @option options [String,Regexp,nil] href    Value to match against the links href, if `nil` finds link placeholders (`<a>` elements with no href attribute), if `false` ignores the href
      #   @option options [String, Regexp] id         Match links with the id provided
      #   @option options [String] title              Match links with the title provided
      #   @option options [String] alt                Match links with a contained img element whose alt matches
      #   @option options [String, Boolean] download  Match links with the download provided
      #   @option options [String] target             Match links with the target provided
      #   @option options [String, Array<String>, Regexp] class    Match links that match the class(es) provided
      # @return [Capybara::Node::Element]   The found element
      #
      def find_link(locator = nil, **options, &optional_filter_block)
        find(:link, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Find a button on the page.
      # 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 find_button([locator], **options)
      #   @param [String] locator            id, name, {Capybara.configure test_id} attribute, value, title, text content, alt of image
      #
      #   @macro waiting_behavior
      #
      #   @option options [Boolean, Symbol] disabled (false)  Match disabled button?
      #                                                       * true - only finds a disabled button
      #                                                       * false - only finds an enabled button
      #                                                       * :all - finds either an enabled or disabled button
      #   @option options [String, Regexp] id                 Match buttons with the id provided
      #   @option options [String] name                       Match buttons with the name provided
      #   @option options [String] title                      Match buttons with the title provided
      #   @option options [String] value                      Match buttons with the value provided
      #   @option options [String, Array<String>, Regexp] class    Match buttons that match the class(es) provided
      # @return [Capybara::Node::Element]   The found element
      #
      def find_button(locator = nil, **options, &optional_filter_block)
        find(:button, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Find a element on the page, given its id.
      #
      # @macro waiting_behavior
      #
      # @param [String] id                  id of element
      #
      # @return [Capybara::Node::Element]   The found element
      #
      def find_by_id(id, **options, &optional_filter_block)
        find(:id, id, **options, &optional_filter_block)
      end

      ##
      # @!method all([kind = Capybara.default_selector], locator = nil, **options)
      #
      # Find all elements on the page matching the given selector
      # and options.
      #
      # Both XPath and CSS expressions are supported, but Capybara
      # does not try to automatically distinguish between them. The
      # following statements are equivalent:
      #
      #     page.all(:css, 'a#person_123')
      #     page.all(:xpath, './/a[@id="person_123"]')
      #
      # If the type of selector is left out, Capybara uses
      # {Capybara.configure default_selector}. It's set to `:css` by default.
      #
      #     page.all("a#person_123")
      #
      #     Capybara.default_selector = :xpath
      #     page.all('.//a[@id="person_123"]')
      #
      # The set of found elements can further be restricted by specifying
      # options. It's possible to select elements by their text or visibility:
      #
      #     page.all('a', text: 'Home')
      #     page.all('#menu li', visible: true)
      #
      # By default Capybara's waiting behavior will wait up to {Capybara.configure default_max_wait_time}
      # seconds for matching elements to be available and then return an empty result if none
      # are available. It is possible to set expectations on the number of results located and
      # Capybara will raise an exception if the number of elements located don't satisfy the
      # specified conditions. The expectations can be set using:
      #
      #     page.assert_selector('p#foo', count: 4)
      #     page.assert_selector('p#foo', maximum: 10)
      #     page.assert_selector('p#foo', minimum: 1)
      #     page.assert_selector('p#foo', between: 1..10)
      #
      # @param [Symbol] kind                       Optional selector type (:css, :xpath, :field, etc.). Defaults to {Capybara.configure default_selector}.
      # @param [String] locator                    The locator for the specified selector
      # @macro system_filters
      # @macro waiting_behavior
      # @option options [Integer] count            Exact number of matches that are expected to be found
      # @option options [Integer] maximum          Maximum number of matches that are expected to be found
      # @option options [Integer] minimum          Minimum number of matches that are expected to be found
      # @option options [Range]   between          Number of matches found must be within the given range
      # @option options [Boolean] allow_reload     Beta feature - May be removed in any version.
      #   When `true` allows elements to be reloaded if they become stale. This is an advanced behavior and should only be used
      #   if you fully understand the potential ramifications. The results can be confusing on dynamic pages. Defaults to `false`
      # @overload all([kind = Capybara.default_selector], locator = nil, **options)
      # @overload all([kind = Capybara.default_selector], locator = nil, **options, &filter_block)
      #   @yieldparam element [Capybara::Node::Element]  The element being considered for inclusion in the results
      #   @yieldreturn [Boolean]                     Should the element be considered in the results?
      # @return [Capybara::Result]                   A collection of found elements
      # @raise [Capybara::ExpectationNotMet]         The number of elements found doesn't match the specified conditions
      def all(*args, allow_reload: false, **options, &optional_filter_block)
        minimum_specified = options_include_minimum?(options)
        options = { minimum: 1 }.merge(options) unless minimum_specified
        options[:session_options] = session_options
        query = Capybara::Queries::SelectorQuery.new(*args, **options, &optional_filter_block)
        result = nil
        begin
          synchronize(query.wait) do
            result = query.resolve_for(self)
            result.allow_reload! if allow_reload
            raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count?

            result
          end
        rescue Capybara::ExpectationNotMet
          raise if minimum_specified || (result.compare_count == 1)

          Result.new([], nil)
        end
      end
      alias_method :find_all, :all

      ##
      #
      # Find the first element on the page matching the given selector
      # and options. By default {#first} will wait up to {Capybara.configure default_max_wait_time}
      # seconds for matching elements to appear and then raise an error if no matching
      # element is found, or `nil` if the provided count options allow for empty results.
      #
      # @overload first([kind], locator, options)
      #   @param [Symbol] kind                       The type of selector
      #   @param [String] locator                    The selector
      #   @param [Hash] options                      Additional options; see {#all}
      # @return [Capybara::Node::Element]            The found element or nil
      # @raise  [Capybara::ElementNotFound]          If element(s) matching the provided options can't be found before time expires
      #
      def first(*args, **options, &optional_filter_block)
        options = { minimum: 1 }.merge(options) unless options_include_minimum?(options)
        all(*args, **options, &optional_filter_block).first
      end

    private

      def synced_resolve(query)
        synchronize(query.wait) do
          if prefer_exact?(query)
            result = query.resolve_for(self, true)
            result = query.resolve_for(self, false) if result.empty? && query.supports_exact? && !query.exact?
          else
            result = query.resolve_for(self)
          end

          if ambiguous?(query, result)
            raise Capybara::Ambiguous, "Ambiguous match, found #{result.size} elements matching #{query.applied_description}"
          end
          raise Capybara::ElementNotFound, "Unable to find #{query.applied_description}" if result.empty?

          result.first
        end.tap(&:allow_reload!)
      end

      def ambiguous?(query, result)
        %i[one smart].include?(query.match) && (result.size > 1)
      end

      def prefer_exact?(query)
        %i[smart prefer_exact].include?(query.match)
      end

      def options_include_minimum?(opts)
        %i[count minimum between].any? { |key| opts.key?(key) }
      end

      def parent
        first(:xpath, './parent::*', minimum: 0)
      end
    end
  end
end


def all(*args, allow_reload: false, **options, &optional_filter_block)
        minimum_specified = options_include_minimum?(options)
        options = { minimum: 1 }.merge(options) unless minimum_specified
        options[:session_options] = session_options
        query = Capybara::Queries::SelectorQuery.new(*args, **options, &optional_filter_block)
        result = nil
        begin
          synchronize(query.wait) do
            result = query.resolve_for(self)
            result.allow_reload! if allow_reload
            raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count?

            result
          end
        rescue Capybara::ExpectationNotMet
          raise if minimum_specified || (result.compare_count == 1)

          Result.new([], nil)
        end
      end


def synced_resolve(query)
        synchronize(query.wait) do
          if prefer_exact?(query)
            result = query.resolve_for(self, true)
            result = query.resolve_for(self, false) if result.empty? && query.supports_exact? && !query.exact?
          else
            result = query.resolve_for(self)
          end

          if ambiguous?(query, result)
            raise Capybara::Ambiguous, "Ambiguous match, found #{result.size} elements matching #{query.applied_description}"
          end
          raise Capybara::ElementNotFound, "Unable to find #{query.applied_description}" if result.empty?

          result.first
        end.tap(&:allow_reload!)
      end


# frozen_string_literal: true

module Capybara
  module Node
    module Matchers
      ##
      #
      # Checks if a given selector is on the page or a descendant of the current node.
      #
      #     page.has_selector?('p#foo')
      #     page.has_selector?(:xpath, './/p[@id="foo"]')
      #     page.has_selector?(:foo)
      #
      # By default it will check if the expression occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_selector?('p.foo', count: 4)
      #
      # This will check if the expression occurs exactly 4 times.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.has_selector?('li', text: 'Horse', visible: true)
      #
      # {#has_selector?} can also accept XPath expressions generated by the
      # XPath gem:
      #
      #     page.has_selector?(:xpath, XPath.descendant(:p))
      #
      # @param (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)     Number of matching elements that should exist
      # @option options [Integer] :minimum (nil)   Minimum number of matching elements that should exist
      # @option options [Integer] :maximum (nil)   Maximum number of matching elements that should exist
      # @option options [Range]   :between (nil)   Range of number of matching elements that should exist
      # @return [Boolean]                       If the expression exists
      #
      def has_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if a given selector is not on the page or a descendant of the current node.
      # Usage is identical to {#has_selector?}.
      #
      # @param (see #has_selector?)
      # @return [Boolean]
      #
      def has_no_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_no_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if a an element has the specified CSS styles.
      #
      #     element.matches_style?( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
      #
      # @param styles [Hash]
      # @return [Boolean]                       If the styles match
      #
      def matches_style?(styles = nil, **options)
        styles, options = options, {} if styles.nil?
        make_predicate(options) { assert_matches_style(styles, **options) }
      end

      ##
      # @deprecated Use {#matches_style?} instead.
      #
      def has_style?(styles = nil, **options)
        Capybara::Helpers.warn "DEPRECATED: has_style? is deprecated, please use matches_style? : #{Capybara::Helpers.filter_backtrace(caller)}"
        matches_style?(styles, **options)
      end

      ##
      #
      # Asserts that a given selector is on the page or a descendant of the current node.
      #
      #     page.assert_selector('p#foo')
      #     page.assert_selector(:xpath, './/p[@id="foo"]')
      #     page.assert_selector(:foo)
      #
      # By default it will check if the expression occurs at least once,
      # but a different number can be specified.
      #
      #     page.assert_selector('p#foo', count: 4)
      #
      # This will check if the expression occurs exactly 4 times. See
      # {Capybara::Node::Finders#all} for other available result size options.
      #
      # If a `:count` of 0 is specified, it will behave like {#assert_no_selector};
      # however, use of that method is preferred over this one.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.assert_selector('li', text: 'Horse', visible: true)
      #
      # {#assert_selector} can also accept XPath expressions generated by the
      # XPath gem:
      #
      #     page.assert_selector(:xpath, XPath.descendant(:p))
      #
      # @param (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)    Number of times the expression should occur
      # @raise [Capybara::ExpectationNotMet]      If the selector does not exist
      #
      def assert_selector(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block) do |result, query|
          unless result.matches_count? && (result.any? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.failure_message
          end
        end
      end

      ##
      #
      # Asserts that an element has the specified CSS styles.
      #
      #     element.assert_matches_style( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
      #
      # @param styles [Hash]
      # @raise [Capybara::ExpectationNotMet]    If the element doesn't have the specified styles
      #
      def assert_matches_style(styles = nil, **options)
        styles, options = options, {} if styles.nil?
        query_args, query_opts = _set_query_session_options(styles, options)
        query = Capybara::Queries::StyleQuery.new(*query_args, **query_opts)
        synchronize(query.wait) do
          raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self)
        end
        true
      end

      ##
      # @deprecated Use {#assert_matches_style} instead.
      #
      def assert_style(styles = nil, **options)
        warn 'assert_style is deprecated, please use assert_matches_style instead'
        assert_matches_style(styles, **options)
      end

      # Asserts that all of the provided selectors are present on the given page
      # or descendants of the current node.  If options are provided, the assertion
      # will check that each locator is present with those options as well (other than `:wait`).
      #
      #     page.assert_all_of_selectors(:custom, 'Tom', 'Joe', visible: all)
      #     page.assert_all_of_selectors(:css, '#my_div', 'a.not_clicked')
      #
      # It accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      # The `:wait` option applies to all of the selectors as a group, so all of the locators must be present
      # within `:wait` (defaults to {Capybara.configure default_max_wait_time}) seconds.
      #
      # @overload assert_all_of_selectors([kind = Capybara.default_selector], *locators, **options)
      #
      def assert_all_of_selectors(*args, **options, &optional_filter_block)
        _verify_multiple(*args, **options) do |selector, locator, opts|
          assert_selector(selector, locator, opts, &optional_filter_block)
        end
      end

      # Asserts that none of the provided selectors are present on the given page
      # or descendants of the current node. If options are provided, the assertion
      # will check that each locator is not present with those options as well (other than `:wait`).
      #
      #     page.assert_none_of_selectors(:custom, 'Tom', 'Joe', visible: all)
      #     page.assert_none_of_selectors(:css, '#my_div', 'a.not_clicked')
      #
      # It accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      # The `:wait` option applies to all of the selectors as a group, so none of the locators must be present
      # within `:wait` (defaults to {Capybara.configure default_max_wait_time}) seconds.
      #
      # @overload assert_none_of_selectors([kind = Capybara.default_selector], *locators, **options)
      #
      def assert_none_of_selectors(*args, **options, &optional_filter_block)
        _verify_multiple(*args, **options) do |selector, locator, opts|
          assert_no_selector(selector, locator, opts, &optional_filter_block)
        end
      end

      # Asserts that any of the provided selectors are present on the given page
      # or descendants of the current node. If options are provided, the assertion
      # will check that each locator is present with those options as well (other than `:wait`).
      #
      #     page.assert_any_of_selectors(:custom, 'Tom', 'Joe', visible: all)
      #     page.assert_any_of_selectors(:css, '#my_div', 'a.not_clicked')
      #
      # It accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      # The `:wait` option applies to all of the selectors as a group, so any of the locators must be present
      # within `:wait` (defaults to {Capybara.configure default_max_wait_time}) seconds.
      #
      # @overload assert_any_of_selectors([kind = Capybara.default_selector], *locators, **options)
      #
      def assert_any_of_selectors(*args, wait: nil, **options, &optional_filter_block)
        wait = session_options.default_max_wait_time if wait.nil?
        selector = extract_selector(args)
        synchronize(wait) do
          res = args.map do |locator|
            assert_selector(selector, locator, options, &optional_filter_block)
            break nil
          rescue Capybara::ExpectationNotMet => e
            e.message
          end
          raise Capybara::ExpectationNotMet, res.join(' or ') if res

          true
        end
      end

      ##
      #
      # Asserts that a given selector is not on the page or a descendant of the current node.
      # Usage is identical to {#assert_selector}.
      #
      # Query options such as `:count`, `:minimum`, `:maximum`, and `:between` are
      # considered to be an integral part of the selector. This will return
      # `true`, for example, if a page contains 4 anchors but the query expects 5:
      #
      #     page.assert_no_selector('a', minimum: 1) # Found, raises Capybara::ExpectationNotMet
      #     page.assert_no_selector('a', count: 4)   # Found, raises Capybara::ExpectationNotMet
      #     page.assert_no_selector('a', count: 5)   # Not Found, returns true
      #
      # @param (see #assert_selector)
      # @raise [Capybara::ExpectationNotMet]      If the selector exists
      #
      def assert_no_selector(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block) do |result, query|
          if result.matches_count? && (!result.empty? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.negative_failure_message
          end
        end
      end

      ##
      #
      # Checks if a given XPath expression is on the page or a descendant of the current node.
      #
      #     page.has_xpath?('.//p[@id="foo"]')
      #
      # By default it will check if the expression occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_xpath?('.//p[@id="foo"]', count: 4)
      #
      # This will check if the expression occurs exactly 4 times.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.has_xpath?('.//li', text: 'Horse', visible: true)
      #
      # {#has_xpath?} can also accept XPath expressions generated by the
      # XPath gem:
      #
      #     xpath = XPath.generate { |x| x.descendant(:p) }
      #     page.has_xpath?(xpath)
      #
      # @param [String] path                      An XPath expression
      # @param options                            (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)    Number of times the expression should occur
      # @return [Boolean]                         If the expression exists
      #
      def has_xpath?(path, **options, &optional_filter_block)
        has_selector?(:xpath, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if a given XPath expression is not on the page or a descendant of the current node.
      # Usage is identical to {#has_xpath?}.
      #
      # @param (see #has_xpath?)
      # @return [Boolean]
      #
      def has_no_xpath?(path, **options, &optional_filter_block)
        has_no_selector?(:xpath, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if a given CSS selector is on the page or a descendant of the current node.
      #
      #     page.has_css?('p#foo')
      #
      # By default it will check if the selector occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_css?('p#foo', count: 4)
      #
      # This will check if the selector occurs exactly 4 times.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.has_css?('li', text: 'Horse', visible: true)
      #
      # @param [String] path                      A CSS selector
      # @param options                            (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)    Number of times the selector should occur
      # @return [Boolean]                         If the selector exists
      #
      def has_css?(path, **options, &optional_filter_block)
        has_selector?(:css, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if a given CSS selector is not on the page or a descendant of the current node.
      # Usage is identical to {#has_css?}.
      #
      # @param (see #has_css?)
      # @return [Boolean]
      #
      def has_no_css?(path, **options, &optional_filter_block)
        has_no_selector?(:css, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a element with the given
      # local name.
      #
      # @param [String] locator           The local name of a element to check for
      # @option options [String, Regexp]  The attributes values of matching elements
      # @return [Boolean]                 Whether it exists
      #
      def has_element?(locator = nil, **options, &optional_filter_block)
        has_selector?(:element, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no element with the given
      # local name.
      #
      # @param (see #has_element?)
      # @return [Boolean]            Whether it doesn't exist
      #
      def has_no_element?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:element, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a link with the given
      # text or id.
      #
      # @param [String] locator           The text or id of a link to check for
      # @option options [String, Regexp] :href    The value the href attribute must be
      # @return [Boolean]                 Whether it exists
      #
      def has_link?(locator = nil, **options, &optional_filter_block)
        has_selector?(:link, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no link with the given
      # text or id.
      #
      # @param (see #has_link?)
      # @return [Boolean]            Whether it doesn't exist
      #
      def has_no_link?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:link, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a button with the given
      # text, value or id.
      #
      # @param [String] locator      The text, value or id of a button to check for
      # @return [Boolean]            Whether it exists
      #
      def has_button?(locator = nil, **options, &optional_filter_block)
        has_selector?(:button, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no button with the given
      # text, value or id.
      #
      # @param [String] locator      The text, value or id of a button to check for
      # @return [Boolean]            Whether it doesn't exist
      #
      def has_no_button?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:button, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a form field with the given
      # label, name or id.
      #
      # For text fields and other textual fields, such as textareas and
      # HTML5 email/url/etc. fields, it's possible to specify a `:with`
      # option to specify the text the field should contain:
      #
      #     page.has_field?('Name', with: 'Jonas')
      #
      # It is also possible to filter by the field type attribute:
      #
      #     page.has_field?('Email', type: 'email')
      #
      # NOTE: 'textarea' and 'select' are valid type values, matching the associated tag names.
      #
      # @param [String] locator                  The label, name or id of a field to check for
      # @option options [String, Regexp] :with   The text content of the field or a Regexp to match
      # @option options [String] :type           The type attribute of the field
      # @return [Boolean]                        Whether it exists
      #
      def has_field?(locator = nil, **options, &optional_filter_block)
        has_selector?(:field, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no form field with the given
      # label, name or id. See {#has_field?}.
      #
      # @param [String] locator                  The label, name or id of a field to check for
      # @option options [String, Regexp] :with   The text content of the field or a Regexp to match
      # @option options [String] :type           The type attribute of the field
      # @return [Boolean]                        Whether it doesn't exist
      #
      def has_no_field?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:field, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a radio button or
      # checkbox with the given label, value, id, or {Capybara.configure test_id} attribute that is currently
      # checked.
      #
      # @param [String] locator           The label, name or id of a checked field
      # @return [Boolean]                 Whether it exists
      #
      def has_checked_field?(locator = nil, **options, &optional_filter_block)
        has_selector?(:field, locator, **options.merge(checked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no radio button or
      # checkbox with the given label, value or id, or {Capybara.configure test_id} attribute that is currently
      # checked.
      #
      # @param [String] locator           The label, name or id of a checked field
      # @return [Boolean]                 Whether it doesn't exist
      #
      def has_no_checked_field?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:field, locator, **options.merge(checked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a radio button or
      # checkbox with the given label, value or id, or {Capybara.configure test_id} attribute that is currently
      # unchecked.
      #
      # @param [String] locator           The label, name or id of an unchecked field
      # @return [Boolean]                 Whether it exists
      #
      def has_unchecked_field?(locator = nil, **options, &optional_filter_block)
        has_selector?(:field, locator, **options.merge(unchecked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no radio button or
      # checkbox with the given label, value or id, or {Capybara.configure test_id} attribute that is currently
      # unchecked.
      #
      # @param [String] locator           The label, name or id of an unchecked field
      # @return [Boolean]                 Whether it doesn't exist
      #
      def has_no_unchecked_field?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:field, locator, **options.merge(unchecked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a select field with the
      # given label, name or id.
      #
      # It can be specified which option should currently be selected:
      #
      #     page.has_select?('Language', selected: 'German')
      #
      # For multiple select boxes, several options may be specified:
      #
      #     page.has_select?('Language', selected: ['English', 'German'])
      #
      # It's also possible to check if the exact set of options exists for
      # this select box:
      #
      #     page.has_select?('Language', options: ['English', 'German', 'Spanish'])
      #
      # You can also check for a partial set of options:
      #
      #     page.has_select?('Language', with_options: ['English', 'German'])
      #
      # @param [String] locator                         The label, name or id of a select box
      # @option options [Array] :options                Options which should be contained in this select box
      # @option options [Array] :with_options           Partial set of options which should be contained in this select box
      # @option options [String, Array] :selected       Options which should be selected
      # @option options [String, Array] :with_selected  Partial set of options which should minimally be selected
      # @return [Boolean]                               Whether it exists
      #
      def has_select?(locator = nil, **options, &optional_filter_block)
        has_selector?(:select, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no select field with the
      # given label, name or id. See {#has_select?}.
      #
      # @param (see #has_select?)
      # @return [Boolean]     Whether it doesn't exist
      #
      def has_no_select?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:select, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a table with the given id
      # or caption:
      #
      #    page.has_table?('People')
      #
      # @param [String] locator  The id or caption of a table
      # @option options [Array<Array<String>>] :rows
      #   Text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
      # @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_rows
      #   Partial set of text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
      # @option options [Array<Array<String>>] :cols
      #   Text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
      # @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_cols
      #   Partial set of text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
      # @return [Boolean]        Whether it exists
      #
      def has_table?(locator = nil, **options, &optional_filter_block)
        has_selector?(:table, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no table with the given id
      # or caption. See {#has_table?}.
      #
      # @param (see #has_table?)
      # @return [Boolean]       Whether it doesn't exist
      #
      def has_no_table?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:table, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Asserts that the current node matches a given selector.
      #
      #     node.assert_matches_selector('p#foo')
      #     node.assert_matches_selector(:xpath, '//p[@id="foo"]')
      #     node.assert_matches_selector(:foo)
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     node.assert_matches_selector('li', text: 'Horse', visible: true)
      #
      # @param (see Capybara::Node::Finders#all)
      # @raise [Capybara::ExpectationNotMet]      If the selector does not match
      #
      def assert_matches_selector(*args, &optional_filter_block)
        _verify_match_result(args, optional_filter_block) do |result|
          raise Capybara::ExpectationNotMet, 'Item does not match the provided selector' unless result.include? self
        end
      end

      ##
      #
      # Asserts that the current node does not match a given selector.
      # Usage is identical to {#assert_matches_selector}.
      #
      # @param (see #assert_matches_selector)
      # @raise [Capybara::ExpectationNotMet]      If the selector matches
      #
      def assert_not_matches_selector(*args, &optional_filter_block)
        _verify_match_result(args, optional_filter_block) do |result|
          raise Capybara::ExpectationNotMet, 'Item matched the provided selector' if result.include? self
        end
      end

      ##
      #
      # Checks if the current node matches given selector.
      #
      # @param (see #has_selector?)
      # @return [Boolean]
      #
      def matches_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_matches_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if the current node matches given XPath expression.
      #
      # @param [String, XPath::Expression] xpath The XPath expression to match against the current code
      # @return [Boolean]
      #
      def matches_xpath?(xpath, **options, &optional_filter_block)
        matches_selector?(:xpath, xpath, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the current node matches given CSS selector.
      #
      # @param [String] css The CSS selector to match against the current code
      # @return [Boolean]
      #
      def matches_css?(css, **options, &optional_filter_block)
        matches_selector?(:css, css, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the current node does not match given selector.
      # Usage is identical to {#has_selector?}.
      #
      # @param (see #has_selector?)
      # @return [Boolean]
      #
      def not_matches_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_not_matches_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if the current node does not match given XPath expression.
      #
      # @param [String, XPath::Expression] xpath The XPath expression to match against the current code
      # @return [Boolean]
      #
      def not_matches_xpath?(xpath, **options, &optional_filter_block)
        not_matches_selector?(:xpath, xpath, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the current node does not match given CSS selector.
      #
      # @param [String] css The CSS selector to match against the current code
      # @return [Boolean]
      #
      def not_matches_css?(css, **options, &optional_filter_block)
        not_matches_selector?(:css, css, **options, &optional_filter_block)
      end

      ##
      # Asserts that the page or current node has the given text content,
      # ignoring any HTML tags.
      #
      # @!macro text_query_params
      #   @overload $0(type, text, **options)
      #     @param [:all, :visible] type               Whether to check for only visible or all text. If this parameter is missing or nil then we use the value of {Capybara.configure ignore_hidden_elements}, which defaults to `true`, corresponding to `:visible`.
      #     @param [String, Regexp] text               The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
      #     @option options [Integer] :count (nil)     Number of times the text is expected to occur
      #     @option options [Integer] :minimum (nil)   Minimum number of times the text is expected to occur
      #     @option options [Integer] :maximum (nil)   Maximum number of times the text is expected to occur
      #     @option options [Range]   :between (nil)   Range of times that is expected to contain number of times text occurs
      #     @option options [Numeric] :wait            Maximum time that Capybara will wait for text to eq/match given string/regexp argument. Defaults to {Capybara.configure default_max_wait_time}.
      #     @option options [Boolean] :exact           Whether text must be an exact match or just substring. Defaults to {Capybara.configure exact_text}.
      #     @option options [Boolean] :normalize_ws (false) When `true` replace all whitespace with standard spaces and collapse consecutive whitespace to a single space
      #   @overload $0(text, **options)
      #     @param [String, Regexp] text               The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
      #     @option options [Integer] :count (nil)     Number of times the text is expected to occur
      #     @option options [Integer] :minimum (nil)   Minimum number of times the text is expected to occur
      #     @option options [Integer] :maximum (nil)   Maximum number of times the text is expected to occur
      #     @option options [Range]   :between (nil)   Range of times that is expected to contain number of times text occurs
      #     @option options [Numeric] :wait            Maximum time that Capybara will wait for text to eq/match given string/regexp argument. Defaults to {Capybara.configure default_max_wait_time}.
      #     @option options [Boolean] :exact           Whether text must be an exact match or just substring. Defaults to {Capybara.configure exact_text}.
      #     @option options [Boolean] :normalize_ws (false) When `true` replace all whitespace with standard spaces and collapse consecutive whitespace to a single space
      # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
      # @return [true]
      #
      def assert_text(type_or_text, *args, **opts)
        _verify_text(type_or_text, *args, **opts) do |count, query|
          unless query.matches_count?(count) && (count.positive? || query.expects_none?)
            raise Capybara::ExpectationNotMet, query.failure_message
          end
        end
      end

      ##
      # Asserts that the page or current node doesn't have the given text content,
      # ignoring any HTML tags.
      #
      # @macro text_query_params
      # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
      # @return [true]
      #
      def assert_no_text(type_or_text, *args, **opts)
        _verify_text(type_or_text, *args, **opts) do |count, query|
          if query.matches_count?(count) && (count.positive? || query.expects_none?)
            raise Capybara::ExpectationNotMet, query.negative_failure_message
          end
        end
      end

      ##
      # Checks if the page or current node has the given text content,
      # ignoring any HTML tags.
      #
      # By default it will check if the text occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_text?('lorem ipsum', between: 2..4)
      #
      # This will check if the text occurs from 2 to 4 times.
      #
      # @macro text_query_params
      # @return [Boolean]                            Whether it exists
      #
      def has_text?(*args, **options)
        make_predicate(options) { assert_text(*args, **options) }
      end
      alias_method :has_content?, :has_text?

      ##
      # Checks if the page or current node does not have the given text
      # content, ignoring any HTML tags and normalizing whitespace.
      #
      # @macro text_query_params
      # @return [Boolean]  Whether it doesn't exist
      #
      def has_no_text?(*args, **options)
        make_predicate(options) { assert_no_text(*args, **options) }
      end
      alias_method :has_no_content?, :has_no_text?

      ##
      #
      # Asserts that a given selector matches an ancestor of the current node.
      #
      #     element.assert_ancestor('p#foo')
      #
      # Accepts the same options as {#assert_selector}
      #
      # @param (see Capybara::Node::Finders#find)
      # @raise [Capybara::ExpectationNotMet]      If the selector does not exist
      #
      def assert_ancestor(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::AncestorQuery) do |result, query|
          unless result.matches_count? && (result.any? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.failure_message
          end
        end
      end

      def assert_no_ancestor(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::AncestorQuery) do |result, query|
          if result.matches_count? && (!result.empty? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.negative_failure_message
          end
        end
      end

      ##
      #
      # Predicate version of {#assert_ancestor}
      #
      def has_ancestor?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_ancestor(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Predicate version of {#assert_no_ancestor}
      #
      def has_no_ancestor?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_no_ancestor(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Asserts that a given selector matches a sibling of the current node.
      #
      #     element.assert_sibling('p#foo')
      #
      # Accepts the same options as {#assert_selector}
      #
      # @param (see Capybara::Node::Finders#find)
      # @raise [Capybara::ExpectationNotMet]      If the selector does not exist
      #
      def assert_sibling(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
          unless result.matches_count? && (result.any? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.failure_message
          end
        end
      end

      def assert_no_sibling(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
          if result.matches_count? && (!result.empty? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.negative_failure_message
          end
        end
      end

      ##
      #
      # Predicate version of {#assert_sibling}
      #
      def has_sibling?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_sibling(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Predicate version of {#assert_no_sibling}
      #
      def has_no_sibling?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_no_sibling(*args, options, &optional_filter_block) }
      end

      def ==(other)
        eql?(other) || (other.respond_to?(:base) && base == other.base)
      end

    private

      def extract_selector(args)
        args.first.is_a?(Symbol) ? args.shift : session_options.default_selector
      end

      def _verify_multiple(*args, wait: nil, **options)
        wait = session_options.default_max_wait_time if wait.nil?
        selector = extract_selector(args)
        synchronize(wait) do
          args.each { |locator| yield(selector, locator, options) }
        end
      end

      def _verify_selector_result(query_args, optional_filter_block, query_type = Capybara::Queries::SelectorQuery)
        # query_args, query_opts = if query_args[0].is_a? Symbol
        #   a,o = _set_query_session_options(*query_args.slice(2..))
        #   [query_args.slice(0..1).concat(a), o]
        # else
        #   _set_query_session_options(*query_args)
        # end
        query_args, query_opts = _set_query_session_options(*query_args)
        query = query_type.new(*query_args, **query_opts, &optional_filter_block)
        synchronize(query.wait) do
          yield query.resolve_for(self), query
        end
        true
      end

      def _verify_match_result(query_args, optional_filter_block)
        query_args, query_opts = _set_query_session_options(*query_args)
        query = Capybara::Queries::MatchQuery.new(*query_args, **query_opts, &optional_filter_block)
        synchronize(query.wait) do
          yield query.resolve_for(parent || session&.document || query_scope)
        end
        true
      end

      def _verify_text(type = nil, expected_text, **query_options) # rubocop:disable Style/OptionalArguments
        query_options[:session_options] = session_options
        query = Capybara::Queries::TextQuery.new(type, expected_text, **query_options)
        synchronize(query.wait) do
          yield query.resolve_for(self), query
        end
        true
      end

      def _set_query_session_options(*query_args)
        query_args, query_options = query_args.dup, {}
        # query_options = query_args.pop if query_options.empty? && query_args.last.is_a?(Hash)
        query_options = query_args.pop if query_args.last.is_a?(Hash)
        query_options[:session_options] = session_options
        [query_args, query_options]
      end

      def make_predicate(options)
        options[:wait] = 0 unless options.key?(:wait) || session_options.predicates_wait
        yield
      rescue Capybara::ExpectationNotMet
        false
      end
    end
  end
end


# frozen_string_literal: true

module Capybara
  module Node
    ##
    #
    # A {Capybara::Node::Simple} is a simpler version of {Capybara::Node::Base} which
    # includes only {Capybara::Node::Finders} and {Capybara::Node::Matchers} and does
    # not include {Capybara::Node::Actions}. This type of node is returned when
    # using {Capybara.string}.
    #
    # It is useful in that it does not require a session, an application or a driver,
    # but can still use Capybara's finders and matchers on any string that contains HTML.
    #
    class Simple
      include Capybara::Node::Finders
      include Capybara::Node::Matchers
      include Capybara::Node::DocumentMatchers

      attr_reader :native

      def initialize(native)
        native = Capybara::HTML(native) if native.is_a?(String)
        @native = native
      end

      ##
      #
      # @return [String]    The text of the element
      #
      def text(_type = nil, normalize_ws: false)
        txt = native.text
        normalize_ws ? txt.gsub(/[[:space:]]+/, ' ').strip : txt
      end

      ##
      #
      # Retrieve the given attribute
      #
      #     element[:title] # => HTML title attribute
      #
      # @param  [Symbol] name  The attribute name to retrieve
      # @return [String]       The value of the attribute
      #
      def [](name)
        attr_name = name.to_s
        if attr_name == 'value'
          value
        elsif (tag_name == 'input') && (native[:type] == 'checkbox') && (attr_name == 'checked')
          native['checked'] == 'checked'
        else
          native[attr_name]
        end
      end

      ##
      #
      # @return [String]      The tag name of the element
      #
      def tag_name
        native.node_name
      end

      ##
      #
      # An XPath expression describing where on the page the element can be found
      #
      # @return [String]      An XPath expression
      #
      def path
        native.path
      end

      ##
      #
      # @return [String]    The value of the form element
      #
      def value
        if tag_name == 'textarea'
          native['_capybara_raw_value']
        elsif tag_name == 'select'
          selected_options = find_xpath('.//option[@selected]')
          if multiple?
            selected_options.map(&method(:option_value))
          else
            option_value(selected_options.first || find_xpath('.//option').first)
          end
        elsif tag_name == 'input' && %w[radio checkbox].include?(native[:type])
          native[:value] || 'on'
        else
          native[:value]
        end
      end

      ##
      #
      # Whether or not the element is visible. Does not support CSS, so
      # the result may be inaccurate.
      #
      # @param  [Boolean] check_ancestors  Whether to inherit visibility from ancestors
      # @return [Boolean]     Whether the element is visible
      #
      def visible?(check_ancestors = true) # rubocop:disable Style/OptionalBooleanParameter
        return false if (tag_name == 'input') && (native[:type] == 'hidden')
        return false if tag_name == 'template'

        if check_ancestors
          !find_xpath(VISIBILITY_XPATH)
        else
          # No need for an xpath if only checking the current element
          !(native.key?('hidden') ||
            /display:\s?none/.match?(native[:style] || '') ||
            %w[script head style].include?(tag_name))
        end
      end

      ##
      #
      # Whether or not the element is checked.
      #
      # @return [Boolean]     Whether the element is checked
      #
      def checked?
        native.has_attribute?('checked')
      end

      ##
      #
      # Whether or not the element is disabled.
      #
      # @return [Boolean]     Whether the element is disabled
      def disabled?
        native.has_attribute?('disabled') &&
          %w[button input select textarea optgroup option menuitem fieldset].include?(tag_name)
      end

      ##
      #
      # Whether or not the element is selected.
      #
      # @return [Boolean]     Whether the element is selected
      #
      def selected?
        native.has_attribute?('selected')
      end

      def multiple?
        native.has_attribute?('multiple')
      end

      def readonly?
        native.has_attribute?('readonly')
      end

      def synchronize(_seconds = nil)
        yield # simple nodes don't need to wait
      end

      def allow_reload!(*)
        # no op
      end

      ##
      #
      # @return [String]     The title of the document
      def title
        native.title
      end

      def inspect
        %(#<Capybara::Node::Simple tag="#{tag_name}" path="#{path}">)
      end

      # @api private
      def find_css(css, **_options)
        native.css(css)
      end

      # @api private
      def find_xpath(xpath, **_options)
        native.xpath(xpath)
      end

      # @api private
      def session_options
        Capybara.session_options
      end

      # @api private
      def initial_cache
        {}
      end

      def ==(other)
        eql?(other) || (other.respond_to?(:native) && native == other.native)
      end

    private

      def option_value(option)
        return nil if option.nil?

        option[:value] || option.content
      end

      VISIBILITY_XPATH = XPath.generate do |x|
        x.ancestor_or_self[
          x.attr(:style)[x.contains('display:none') | x.contains('display: none')] |
          x.attr(:hidden) |
          x.qname.one_of('script', 'head') |
          (~x.self(:summary) & XPath.parent(:details)[!XPath.attr(:open)])
        ].boolean
      end.to_s.freeze
    end
  end
end


def value
        if tag_name == 'textarea'
          native['_capybara_raw_value']
        elsif tag_name == 'select'
          selected_options = find_xpath('.//option[@selected]')
          if multiple?
            selected_options.map(&method(:option_value))
          else
            option_value(selected_options.first || find_xpath('.//option').first)
          end
        elsif tag_name == 'input' && %w[radio checkbox].include?(native[:type])
          native[:value] || 'on'
        else
          native[:value]
        end
      end


def visible?(check_ancestors = true) # rubocop:disable Style/OptionalBooleanParameter
        return false if (tag_name == 'input') && (native[:type] == 'hidden')
        return false if tag_name == 'template'

        if check_ancestors
          !find_xpath(VISIBILITY_XPATH)
        else
          # No need for an xpath if only checking the current element
          !(native.key?('hidden') ||
            /display:\s?none/.match?(native[:style] || '') ||
            %w[script head style].include?(tag_name))
        end
      end


# frozen_string_literal: true

module Capybara
  module Queries
    class AncestorQuery < Capybara::Queries::SelectorQuery
      # @api private
      def resolve_for(node, exact = nil)
        @child_node = node

        node.synchronize do
          scope = node.respond_to?(:session) ? node.session.current_scope : node.find(:xpath, '/*')
          match_results = super(scope, exact)
          ancestors = node.find_xpath(XPath.ancestor.to_s)
                          .map(&method(:to_element))
                          .select { |el| match_results.include?(el) }
          Capybara::Result.new(ordered_results(ancestors), self)
        end
      end

      def description(applied = false) # rubocop:disable Style/OptionalBooleanParameter
        child_query = @child_node&.instance_variable_get(:@query)
        desc = super
        desc += " that is an ancestor of #{child_query.description}" if child_query
        desc
      end
    end
  end
end

class AncestorQuery < < Capybara::Queries::SelectorQuery


# frozen_string_literal: true

module Capybara
  # @api private
  module Queries
    class BaseQuery
      COUNT_KEYS = %i[count minimum maximum between].freeze

      attr_reader :options
      attr_writer :session_options

      def initialize(options)
        @session_options = options.delete(:session_options)
      end

      def session_options
        @session_options || Capybara.session_options
      end

      def wait
        self.class.wait(options, session_options.default_max_wait_time)
      end

      def self.wait(options, default = Capybara.default_max_wait_time)
        # if no value or nil for the :wait option is passed it should default to the default
        wait = options.fetch(:wait, nil)
        wait = default if wait.nil?
        wait || 0
      end

      ##
      #
      # Checks if a count of 0 is valid for the query
      # Returns false if query does not have any count options specified.
      #
      def expects_none?
        count_specified? ? matches_count?(0) : false
      end

      ##
      #
      # Checks if the given count matches the query count options.
      # Defaults to true if no count options are specified. If multiple
      # count options exist, it tests that all conditions are met;
      # however, if :count is specified, all other options are ignored.
      #
      # @param [Integer] count     The actual number. Should be coercible via Integer()
      #
      def matches_count?(count)
        return (Integer(options[:count]) == count) if options[:count]
        return false if options[:maximum] && (Integer(options[:maximum]) < count)
        return false if options[:minimum] && (Integer(options[:minimum]) > count)
        return false if options[:between] && !options[:between].include?(count)

        true
      end

      ##
      #
      # Generates a failure message from the query description and count options.
      #
      def failure_message
        "expected to find #{description}" << count_message
      end

      def negative_failure_message
        "expected not to find #{description}" << count_message
      end

    private

      def count_specified?
        COUNT_KEYS.any? { |key| options.key? key }
      end

      def count_message
        message = +''
        count, between, maximum, minimum = options.values_at(:count, :between, :maximum, :minimum)
        if count
          message << " #{occurrences count}"
        elsif between
          message << " between #{between.begin ? between.first : 1} and " \
                     "#{between.end ? between.last : 'infinite'} times"
        elsif maximum
          message << " at most #{occurrences maximum}"
        elsif minimum
          message << " at least #{occurrences minimum}"
        end
        message
      end

      def occurrences(count)
        "#{count} #{Capybara::Helpers.declension('time', 'times', count)}"
      end

      def assert_valid_keys
        invalid_keys = @options.keys - valid_keys
        return if invalid_keys.empty?

        invalid_names = invalid_keys.map(&:inspect).join(', ')
        valid_names = valid_keys.map(&:inspect).join(', ')
        raise ArgumentError, "Invalid option(s) #{invalid_names}, should be one of #{valid_names}"
      end
    end
  end
end


# frozen_string_literal: true

require 'addressable/uri'

module Capybara
  # @api private
  module Queries
    class CurrentPathQuery < BaseQuery
      def initialize(expected_path, **options, &optional_filter_block)
        super(options)
        @expected_path = expected_path
        @options = {
          url: !@expected_path.is_a?(Regexp) && !::Addressable::URI.parse(@expected_path || '').hostname.nil?,
          ignore_query: false
        }.merge(options)
        @filter_block = optional_filter_block
        assert_valid_keys
      end

      def resolves_for?(session)
        uri = ::Addressable::URI.parse(session.current_url)
        @actual_path = (options[:ignore_query] ? uri&.omit(:query) : uri).then do |u|
          options[:url] ? u&.to_s : u&.request_uri
        end

        res = if @expected_path.is_a? Regexp
          @actual_path.to_s.match?(@expected_path)
        else
          ::Addressable::URI.parse(@expected_path) == ::Addressable::URI.parse(@actual_path)
        end

        res && matches_filter_block?(uri)
      end

      def failure_message
        failure_message_helper
      end

      def negative_failure_message
        failure_message_helper(' not')
      end

    private

      def matches_filter_block?(url)
        return true unless @filter_block

        @filter_block.call(url)
      end

      def failure_message_helper(negated = '')
        verb = @expected_path.is_a?(Regexp) ? 'match' : 'equal'
        "expected #{@actual_path.inspect}#{negated} to #{verb} #{@expected_path.inspect}"
      end

      def valid_keys
        %i[wait url ignore_query]
      end
    end
  end
end


def resolves_for?(session)
        uri = ::Addressable::URI.parse(session.current_url)
        @actual_path = (options[:ignore_query] ? uri&.omit(:query) : uri).then do |u|
          options[:url] ? u&.to_s : u&.request_uri
        end

        res = if @expected_path.is_a? Regexp
          @actual_path.to_s.match?(@expected_path)
        else
          ::Addressable::URI.parse(@expected_path) == ::Addressable::URI.parse(@actual_path)
        end

        res && matches_filter_block?(uri)
      end