# frozen_string_literal: true

require 'uri'
require 'English'

class Capybara::Selenium::Driver < Capybara::Driver::Base
  include Capybara::Selenium::Find

  DEFAULT_OPTIONS = {
    browser: :firefox,
    clear_local_storage: nil,
    clear_session_storage: nil
  }.freeze
  SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout native_displayed].freeze
  CAPS_VERSION = Gem::Requirement.new('< 4.8.0')

  attr_reader :app, :options

  class << self
    attr_reader :selenium_webdriver_version

    def load_selenium
      require 'selenium-webdriver'
      require 'capybara/selenium/patches/atoms'
      require 'capybara/selenium/patches/is_displayed'

      # Look up the version of `selenium-webdriver` to
      # see if it's a version we support.
      #
      # By default, we use Gem.loaded_specs to determine
      # the version number. However, in some cases, such
      # as when loading `selenium-webdriver` outside of
      # Rubygems, we fall back to referencing
      # Selenium::WebDriver::VERSION. Ideally we'd
      # use the constant in all cases, but earlier versions
      # of `selenium-webdriver` didn't provide the constant.
      @selenium_webdriver_version =
        if Gem.loaded_specs['selenium-webdriver']
          Gem.loaded_specs['selenium-webdriver'].version
        else
          Gem::Version.new(Selenium::WebDriver::VERSION)
        end

      unless Gem::Requirement.new('>= 4.8').satisfied_by? @selenium_webdriver_version
        warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade to 4.8+."
      end

      @selenium_webdriver_version
    rescue LoadError => e
      raise e unless e.message.include?('selenium-webdriver')

      raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
    end

    attr_reader :specializations

    def register_specialization(browser_name, specialization)
      @specializations ||= {}
      @specializations[browser_name] = specialization
    end
  end

  def browser
    unless @browser
      options[:http_client] ||= begin
        require 'capybara/selenium/patches/persistent_client'
        if options[:timeout]
          ::Capybara::Selenium::PersistentClient.new(read_timeout: options[:timeout])
        else
          ::Capybara::Selenium::PersistentClient.new
        end
      end
      processed_options = options.except(*SPECIAL_OPTIONS)

      @browser = Selenium::WebDriver.for(options[:browser], processed_options)

      specialize_driver
      setup_exit_handler
    end
    @browser
  end

  def initialize(app, **options)
    super()
    self.class.load_selenium
    @app = app
    @browser = nil
    @exit_status = nil
    @frame_handles = Hash.new { |hash, handle| hash[handle] = [] }
    @options = DEFAULT_OPTIONS.merge(options)
    @node_class = ::Capybara::Selenium::Node
  end

  def visit(path)
    browser.navigate.to(path)
  end

  def refresh
    browser.navigate.refresh
  end

  def go_back
    browser.navigate.back
  end

  def go_forward
    browser.navigate.forward
  end

  def html
    browser.page_source
  rescue Selenium::WebDriver::Error::JavascriptError => e
    raise unless e.message.include?('documentElement is null')
  end

  def title
    browser.title
  end

  def current_url
    browser.current_url
  end

  def wait?; true; end
  def needs_server?; true; end

  def execute_script(script, *args)
    browser.execute_script(script, *native_args(args))
  end

  def evaluate_script(script, *args)
    result = execute_script("return #{script}", *args)
    unwrap_script_result(result)
  end

  def evaluate_async_script(script, *args)
    browser.manage.timeouts.script_timeout = Capybara.default_max_wait_time
    result = browser.execute_async_script(script, *native_args(args))
    unwrap_script_result(result)
  end

  def active_element
    build_node(native_active_element)
  end

  def send_keys(*args)
    # Should this call the specialized nodes rather than native???
    native_active_element.send_keys(*args)
  end

  def save_screenshot(path, **options)
    browser.save_screenshot(path, **options)
  end

  def reset!
    # Use instance variable directly so we avoid starting the browser just to reset the session
    return unless @browser

    navigated = false
    timer = Capybara::Helpers.timer(expire_in: 10)
    begin
      # Only trigger a navigation if we haven't done it already, otherwise it
      # can trigger an endless series of unload modals
      reset_browser_state unless navigated
      navigated = true
      # Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
      wait_for_empty_page(timer)
    rescue *unhandled_alert_errors
      # This error is thrown if an unhandled alert is on the page
      # Firefox appears to automatically dismiss this alert, chrome does not
      # We'll try to accept it
      accept_unhandled_reset_alert
      # try cleaning up the browser again
      retry
    end
  end

  def frame_obscured_at?(x:, y:)
    frame = @frame_handles[current_window_handle].last
    return false unless frame

    switch_to_frame(:parent)
    begin
      frame.base.obscured?(x: x, y: y)
    ensure
      switch_to_frame(frame)
    end
  end

  def switch_to_frame(frame)
    handles = @frame_handles[current_window_handle]
    case frame
    when :top
      handles.clear
      browser.switch_to.default_content
    when :parent
      handles.pop
      browser.switch_to.parent_frame
    else
      handles << frame
      browser.switch_to.frame(frame.native)
    end
  end

  def current_window_handle
    browser.window_handle
  end

  def window_size(handle)
    within_given_window(handle) do
      size = browser.manage.window.size
      [size.width, size.height]
    end
  end

  def resize_window_to(handle, width, height)
    within_given_window(handle) do
      browser.manage.window.resize_to(width, height)
    end
  end

  def maximize_window(handle)
    within_given_window(handle) do
      browser.manage.window.maximize
    end
    sleep 0.1 # work around for https://code.google.com/p/selenium/issues/detail?id=7405
  end

  def fullscreen_window(handle)
    within_given_window(handle) do
      browser.manage.window.full_screen
    end
  end

  def close_window(handle)
    raise ArgumentError, 'Not allowed to close the primary window' if handle == window_handles.first

    within_given_window(handle) do
      browser.close
    end
  end

  def window_handles
    browser.window_handles
  end

  def open_new_window(kind = :tab)
    if browser.switch_to.respond_to?(:new_window)
      handle = current_window_handle
      browser.switch_to.new_window(kind)
      switch_to_window(handle)
    else
      browser.manage.new_window(kind)
    end
  rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
    # If not supported by the driver or browser default to using JS
    browser.execute_script('window.open();')
  end

  def switch_to_window(handle)
    browser.switch_to.window handle
  end

  def accept_modal(_type, **options)
    yield if block_given?
    modal = find_modal(**options)

    modal.send_keys options[:with] if options[:with]

    message = modal.text
    modal.accept
    message
  end

  def dismiss_modal(_type, **options)
    yield if block_given?
    modal = find_modal(**options)
    message = modal.text
    modal.dismiss
    message
  end

  def quit
    @browser&.quit
  rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED,
         Selenium::WebDriver::Error::InvalidSessionIdError
    # Browser must have already gone
  rescue Selenium::WebDriver::Error::UnknownError => e
    unless silenced_unknown_error_message?(e.message) # Most likely already gone
      # probably already gone but not sure - so warn
      warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
    end
  ensure
    @browser = nil
  end

  def invalid_element_errors
    @invalid_element_errors ||=
      [
        ::Selenium::WebDriver::Error::StaleElementReferenceError,
        ::Selenium::WebDriver::Error::ElementNotInteractableError,
        ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around chromedriver go_back/go_forward race condition
        ::Selenium::WebDriver::Error::ElementClickInterceptedError,
        ::Selenium::WebDriver::Error::NoSuchElementError, # IE
        ::Selenium::WebDriver::Error::InvalidArgumentError # IE
      ].tap do |errors|
        if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
          errors.push(::Selenium::WebDriver::Error::DetachedShadowRootError)
        end
      end
  end

  def no_such_window_error
    Selenium::WebDriver::Error::NoSuchWindowError
  end

private

  def native_args(args)
    args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
  end

  def native_active_element
    browser.switch_to.active_element
  end

  def clear_browser_state
    delete_all_cookies
    clear_storage
  rescue *clear_browser_state_errors
    # delete_all_cookies fails when we've previously gone
    # to about:blank, so we rescue this error and do nothing
    # instead.
  end

  def clear_browser_state_errors
    @clear_browser_state_errors ||= [Selenium::WebDriver::Error::UnknownError]
  end

  def unhandled_alert_errors
    @unhandled_alert_errors ||= [Selenium::WebDriver::Error::UnexpectedAlertOpenError]
  end

  def delete_all_cookies
    @browser.manage.delete_all_cookies
  end

  def clear_storage
    clear_session_storage unless options[:clear_session_storage] == false
    clear_local_storage unless options[:clear_local_storage] == false
  rescue Selenium::WebDriver::Error::JavascriptError, Selenium::WebDriver::Error::WebDriverError
    # session/local storage may not be available if on non-http pages (e.g. about:blank)
  end

  def clear_session_storage
    if @browser.respond_to? :session_storage
      @browser.session_storage.clear
    else
      begin
        @browser&.execute_script('window.sessionStorage.clear()')
      rescue # rubocop:disable Style/RescueStandardError
        unless options[:clear_session_storage].nil?
          warn 'sessionStorage clear requested but is not supported by this driver'
        end
      end
    end
  end

  def clear_local_storage
    # selenium-webdriver 4.30.0 removed HTML5 storage accessors -- not really sure why
    # can we replicate this robustly via CDP?
    if @browser.respond_to? :local_storage
      @browser.local_storage.clear
    else
      begin
        @browser&.execute_script('window.localStorage.clear()')
      rescue # rubocop:disable Style/RescueStandardError
        unless options[:clear_local_storage].nil?
          warn 'localStorage clear requested but is not supported by this driver'
        end
      end
    end
  end

  def navigate_with_accept(url)
    @browser.navigate.to(url)
    sleep 0.1 # slight wait for alert
    @browser.switch_to.alert.accept
  rescue modal_error
    # alert now gone, should mean navigation happened
  end

  def modal_error
    Selenium::WebDriver::Error::NoSuchAlertError
  end

  def within_given_window(handle)
    original_handle = current_window_handle
    if handle == original_handle
      yield
    else
      switch_to_window(handle)
      result = yield
      switch_to_window(original_handle)
      result
    end
  end

  def find_modal(text: nil, **options)
    # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
    # Actual wait time may be longer than specified
    wait = Selenium::WebDriver::Wait.new(
      timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0,
      ignore: modal_error
    )
    begin
      wait.until do
        alert = @browser.switch_to.alert
        regexp = text.is_a?(Regexp) ? text : Regexp.new(Regexp.escape(text.to_s))
        matched = alert.text.match?(regexp)
        unless matched
          raise Capybara::ModalNotFound, "Unable to find modal dialog with #{text} - found '#{alert.text}' instead."
        end

        alert
      end
    rescue *find_modal_errors
      raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
    end
  end

  def find_modal_errors
    @find_modal_errors ||= [Selenium::WebDriver::Error::TimeoutError]
  end

  def silenced_unknown_error_message?(msg)
    silenced_unknown_error_messages.any? { |regex| msg.match? regex }
  end

  def silenced_unknown_error_messages
    [/Error communicating with the remote browser/]
  end

  def unwrap_script_result(arg)
    case arg
    when Array
      arg.map { |arr| unwrap_script_result(arr) }
    when Hash
      arg.transform_values! { |value| unwrap_script_result(value) }
    when Selenium::WebDriver::Element, Selenium::WebDriver::ShadowRoot
      build_node(arg)
    else
      arg
    end
  end

  def find_context
    browser
  end

  def build_node(native_node, initial_cache = {})
    ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
  end

  def bridge
    browser.send(:bridge)
  end

  def specialize_driver
    browser_type = browser.browser
    Capybara::Selenium::Driver.specializations.select { |k, _v| k === browser_type }.each_value do |specialization| # rubocop:disable Style/CaseEquality
      extend specialization
    end
  end

  def setup_exit_handler
    main = Process.pid
    at_exit do
      # Store the exit status of the test run since it goes away after calling the at_exit proc...
      @exit_status = $ERROR_INFO.status if $ERROR_INFO.is_a?(SystemExit)
      quit if Process.pid == main
      exit @exit_status if @exit_status # Force exit with stored status
    end
  end

  def reset_browser_state
    clear_browser_state
    @browser.navigate.to('about:blank')
  end

  def wait_for_empty_page(timer)
    until find_xpath('/html/body/*').empty?
      raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?

      sleep 0.01

      # It has been observed that it is possible that asynchronous JS code in
      # the application under test can navigate the browser away from about:blank
      # if the timing is just right. Ensure we are still at about:blank...
      @browser.navigate.to('about:blank') unless current_url == 'about:blank'
    end
  end

  def accept_unhandled_reset_alert
    @browser.switch_to.alert.accept
    sleep 0.25 # allow time for the modal to be handled
  rescue modal_error
    # The alert is now gone.
    # If navigation has not occurred attempt again and accept alert
    # since FF may have dismissed the alert at first attempt.
    navigate_with_accept('about:blank') if current_url != 'about:blank'
  end
end

require 'capybara/selenium/driver_specializations/chrome_driver'
require 'capybara/selenium/driver_specializations/firefox_driver'
require 'capybara/selenium/driver_specializations/internet_explorer_driver'
require 'capybara/selenium/driver_specializations/safari_driver'
require 'capybara/selenium/driver_specializations/edge_driver'

class Capybara::Selenium::Driver < < Capybara::Driver::Base


def browser
    unless @browser
      options[:http_client] ||= begin
        require 'capybara/selenium/patches/persistent_client'
        if options[:timeout]
          ::Capybara::Selenium::PersistentClient.new(read_timeout: options[:timeout])
        else
          ::Capybara::Selenium::PersistentClient.new
        end
      end
      processed_options = options.except(*SPECIAL_OPTIONS)

      @browser = Selenium::WebDriver.for(options[:browser], processed_options)

      specialize_driver
      setup_exit_handler
    end
    @browser
  end


def initialize(app, **options)
    super()
    self.class.load_selenium
    @app = app
    @browser = nil
    @exit_status = nil
    @frame_handles = Hash.new { |hash, handle| hash[handle] = [] }
    @options = DEFAULT_OPTIONS.merge(options)
    @node_class = ::Capybara::Selenium::Node
  end


def reset!
    # Use instance variable directly so we avoid starting the browser just to reset the session
    return unless @browser

    navigated = false
    timer = Capybara::Helpers.timer(expire_in: 10)
    begin
      # Only trigger a navigation if we haven't done it already, otherwise it
      # can trigger an endless series of unload modals
      reset_browser_state unless navigated
      navigated = true
      # Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
      wait_for_empty_page(timer)
    rescue *unhandled_alert_errors
      # This error is thrown if an unhandled alert is on the page
      # Firefox appears to automatically dismiss this alert, chrome does not
      # We'll try to accept it
      accept_unhandled_reset_alert
      # try cleaning up the browser again
      retry
    end
  end


def frame_obscured_at?(x:, y:)
    frame = @frame_handles[current_window_handle].last
    return false unless frame

    switch_to_frame(:parent)
    begin
      frame.base.obscured?(x: x, y: y)
    ensure
      switch_to_frame(frame)
    end
  end


def switch_to_frame(frame)
    handles = @frame_handles[current_window_handle]
    case frame
    when :top
      handles.clear
      browser.switch_to.default_content
    when :parent
      handles.pop
      browser.switch_to.parent_frame
    else
      handles << frame
      browser.switch_to.frame(frame.native)
    end
  end


def open_new_window(kind = :tab)
    if browser.switch_to.respond_to?(:new_window)
      handle = current_window_handle
      browser.switch_to.new_window(kind)
      switch_to_window(handle)
    else
      browser.manage.new_window(kind)
    end
  rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
    # If not supported by the driver or browser default to using JS
    browser.execute_script('window.open();')
  end


def quit
    @browser&.quit
  rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED,
         Selenium::WebDriver::Error::InvalidSessionIdError
    # Browser must have already gone
  rescue Selenium::WebDriver::Error::UnknownError => e
    unless silenced_unknown_error_message?(e.message) # Most likely already gone
      # probably already gone but not sure - so warn
      warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
    end
  ensure
    @browser = nil
  end


def invalid_element_errors
    @invalid_element_errors ||=
      [
        ::Selenium::WebDriver::Error::StaleElementReferenceError,
        ::Selenium::WebDriver::Error::ElementNotInteractableError,
        ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around chromedriver go_back/go_forward race condition
        ::Selenium::WebDriver::Error::ElementClickInterceptedError,
        ::Selenium::WebDriver::Error::NoSuchElementError, # IE
        ::Selenium::WebDriver::Error::InvalidArgumentError # IE
      ].tap do |errors|
        if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
          errors.push(::Selenium::WebDriver::Error::DetachedShadowRootError)
        end
      end
  end


def clear_session_storage
    if @browser.respond_to? :session_storage
      @browser.session_storage.clear
    else
      begin
        @browser&.execute_script('window.sessionStorage.clear()')
      rescue # rubocop:disable Style/RescueStandardError
        unless options[:clear_session_storage].nil?
          warn 'sessionStorage clear requested but is not supported by this driver'
        end
      end
    end
  end


def clear_local_storage
    # selenium-webdriver 4.30.0 removed HTML5 storage accessors -- not really sure why
    # can we replicate this robustly via CDP?
    if @browser.respond_to? :local_storage
      @browser.local_storage.clear
    else
      begin
        @browser&.execute_script('window.localStorage.clear()')
      rescue # rubocop:disable Style/RescueStandardError
        unless options[:clear_local_storage].nil?
          warn 'localStorage clear requested but is not supported by this driver'
        end
      end
    end
  end


def within_given_window(handle)
    original_handle = current_window_handle
    if handle == original_handle
      yield
    else
      switch_to_window(handle)
      result = yield
      switch_to_window(original_handle)
      result
    end
  end


def find_modal(text: nil, **options)
    # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
    # Actual wait time may be longer than specified
    wait = Selenium::WebDriver::Wait.new(
      timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0,
      ignore: modal_error
    )
    begin
      wait.until do
        alert = @browser.switch_to.alert
        regexp = text.is_a?(Regexp) ? text : Regexp.new(Regexp.escape(text.to_s))
        matched = alert.text.match?(regexp)
        unless matched
          raise Capybara::ModalNotFound, "Unable to find modal dialog with #{text} - found '#{alert.text}' instead."
        end

        alert
      end
    rescue *find_modal_errors
      raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
    end
  end


def unwrap_script_result(arg)
    case arg
    when Array
      arg.map { |arr| unwrap_script_result(arr) }
    when Hash
      arg.transform_values! { |value| unwrap_script_result(value) }
    when Selenium::WebDriver::Element, Selenium::WebDriver::ShadowRoot
      build_node(arg)
    else
      arg
    end
  end


# frozen_string_literal: true

# Selenium specific implementation of the Capybara::Driver::Node API

require 'capybara/selenium/extensions/find'
require 'capybara/selenium/extensions/scroll'
require 'capybara/node/whitespace_normalizer'

class Capybara::Selenium::Node < Capybara::Driver::Node
  include Capybara::Node::WhitespaceNormalizer
  include Capybara::Selenium::Find
  include Capybara::Selenium::Scroll

  def visible_text
    raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?

    native.text
  end

  def all_text
    text = driver.evaluate_script('arguments[0].textContent', self) || ''
    normalize_spacing(text)
  end

  def [](name)
    native.attribute(name.to_s)
  rescue Selenium::WebDriver::Error::WebDriverError
    nil
  end

  def value
    if tag_name == 'select' && multiple?
      native.find_elements(:css, 'option:checked').map { |el| el[:value] || el.text }
    else
      native[:value]
    end
  end

  def style(styles)
    styles.to_h { |style| [style, native.css_value(style)] }
  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
  # @option options [Symbol,Array] :clear (nil) The method used to clear the previous value

  #   nil => clear via javascript

  #   :none =>  append the new value to the existing value

  #   :backspace => send backspace keystrokes to clear the field

  #   Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
  # @option options [Boolean] :rapid (nil) Whether setting text inputs should use a faster "rapid" mode

  #   nil => Text inputs with length greater than 30 characters will be set using a faster driver script mode

  #   true => Rapid mode will be used regardless of input length

  #   false => Sends keys via conventional mode. This may be required to avoid losing key-presses if you have certain
  #            Javascript interactions on form inputs

  def set(value, **options)
    if value.is_a?(Array) && !multiple?
      raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
    end

    tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
    @tag_name ||= tag_name

    case tag_name
    when 'input'
      case type
      when 'radio'
        click
      when 'checkbox'
        click if value ^ checked?
      when 'file'
        set_file(value)
      when 'date'
        set_date(value)
      when 'time'
        set_time(value)
      when 'datetime-local'
        set_datetime_local(value)
      when 'color'
        set_color(value)
      when 'range'
        set_range(value)
      else
        set_text(value, **options)
      end
    when 'textarea'
      set_text(value, **options)
    else
      set_content_editable(value)
    end
  end

  def select_option
    click unless selected? || disabled?
  end

  def unselect_option
    raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?

    click if selected?
  end

  def click(keys = [], **options)
    click_options = ClickOptions.new(keys, options)
    return native.click if click_options.empty?

    perform_with_options(click_options) do |action|
      target = click_options.coords? ? nil : native
      if click_options.delay.zero?
        action.click(target)
      else
        action.click_and_hold(target)
        action_pause(action, click_options.delay)
        action.release
      end
    end
  rescue StandardError => e
    if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
       e.message.include?('Other element would receive the click')
      scroll_to_center
    end

    raise e
  end

  def right_click(keys = [], **options)
    click_options = ClickOptions.new(keys, options)
    perform_with_options(click_options) do |action|
      target = click_options.coords? ? nil : native
      if click_options.delay.zero?
        action.context_click(target)
      else
        action.move_to(target) if target
        action.pointer_down(:right).then do |act|
          action_pause(act, click_options.delay)
        end.pointer_up(:right)
      end
    end
  end

  def double_click(keys = [], **options)
    click_options = ClickOptions.new(keys, options)
    raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?

    perform_with_options(click_options) do |action|
      click_options.coords? ? action.double_click : action.double_click(native)
    end
  end

  def send_keys(*args)
    native.send_keys(*args)
  end

  def hover
    scroll_if_needed { browser_action.move_to(native).perform }
  end

  def drag_to(element, drop_modifiers: [], **)
    drop_modifiers = Array(drop_modifiers)
    # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
    # which means Seleniums `drag_and_drop` is now broken - do it manually
    scroll_if_needed { browser_action.click_and_hold(native).perform }
    # element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
    element.scroll_if_needed do
      keys_down = modifiers_down(browser_action, drop_modifiers)
      keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
      keys_up.perform
    end
  end

  def drop(*_)
    raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
  end

  def tag_name
    @tag_name ||=
      if native.respond_to? :tag_name
        native.tag_name.downcase
      else
        shadow_root? ? 'ShadowRoot' : 'Unknown'
      end
  end

  def visible?; boolean_attr(native.displayed?); end
  def readonly?; boolean_attr(self[:readonly]); end
  def multiple?; boolean_attr(self[:multiple]); end
  def selected?; boolean_attr(native.selected?); end
  alias :checked? :selected?

  def disabled?
    return true unless native.enabled?

    # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
    find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
  end

  def content_editable?
    native.attribute('isContentEditable') == 'true'
  end

  def path
    driver.evaluate_script GET_XPATH_SCRIPT, self
  end

  def obscured?(x: nil, y: nil)
    res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
    return true if res == true

    driver.frame_obscured_at?(x: res['x'], y: res['y'])
  end

  def rect
    native.rect
  end

  def shadow_root
    root = native.shadow_root
    root && build_node(native.shadow_root)
  end

protected

  def scroll_if_needed
    yield
  rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
    scroll_to_center
    yield
  end

  def scroll_to_center
    script = <<-JS
      try {
        arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
      } catch(e) {
        arguments[0].scrollIntoView(true);
      }
    JS
    begin
      driver.execute_script(script, self)
    rescue StandardError
      # Swallow error if scrollIntoView with options isn't supported
    end
  end

private

  def sibling_index(parent, node, selector)
    siblings = parent.find_xpath(selector)
    case siblings.size
    when 0
      '[ERROR]' # IE doesn't support full XPath (namespace-uri, etc)
    when 1
      '' # index not necessary when only one matching element
    else
      idx = siblings.index(node)
      # Element may not be found in the siblings if it has gone away
      idx.nil? ? '[ERROR]' : "[#{idx + 1}]"
    end
  end

  def boolean_attr(val)
    val && (val != 'false')
  end

  # a reference to the select node if this is an option node
  def select_node
    find_xpath(XPath.ancestor(:select)[1]).first
  end

  def set_text(value, clear: nil, rapid: nil, **_unused)
    value = value.to_s
    if value.empty? && clear.nil?
      native.clear
    elsif clear == :backspace
      # Clear field by sending the correct number of backspace keys.
      backspaces = [:backspace] * self.value.to_s.length
      send_keys(:end, *backspaces, value)
    elsif clear.is_a? Array
      send_keys(*clear, value)
    else
      driver.execute_script 'arguments[0].select()', self unless clear == :none
      if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
        send_keys(value[0..3])
        driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
        send_keys(value[-3..])
      else
        send_keys(value)
      end
    end
  end

  def auto_rapid_set_length
    30
  end

  def perform_with_options(click_options, &block)
    raise ArgumentError, 'A block must be provided' unless block

    scroll_if_needed do
      action_with_modifiers(click_options) do |action|
        if block
          yield action
        else
          click_options.coords? ? action.click : action.click(native)
        end
      end
    end
  end

  def set_date(value) # rubocop:disable Naming/AccessorMethodName
    value = SettableValue.new(value)
    return set_text(value) unless value.dateable?

    # TODO: this would be better if locale can be detected and correct keystrokes sent
    update_value_js(value.to_date_str)
  end

  def set_time(value) # rubocop:disable Naming/AccessorMethodName
    value = SettableValue.new(value)
    return set_text(value) unless value.timeable?

    # TODO: this would be better if locale can be detected and correct keystrokes sent
    update_value_js(value.to_time_str)
  end

  def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
    value = SettableValue.new(value)
    return set_text(value) unless value.timeable?

    # TODO: this would be better if locale can be detected and correct keystrokes sent
    update_value_js(value.to_datetime_str)
  end

  def set_color(value) # rubocop:disable Naming/AccessorMethodName
    update_value_js(value)
  end

  def set_range(value) # rubocop:disable Naming/AccessorMethodName
    update_value_js(value)
  end

  def update_value_js(value)
    driver.execute_script(<<-JS, self, value)
      if (arguments[0].readOnly) { return };
      if (document.activeElement !== arguments[0]){
        arguments[0].focus();
      }
      if (arguments[0].value != arguments[1]) {
        arguments[0].value = arguments[1]
        arguments[0].dispatchEvent(new InputEvent('input'));
        arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
      }
    JS
  end

  def set_file(value) # rubocop:disable Naming/AccessorMethodName
    with_file_detector do
      path_names = value.to_s.empty? ? [] : value
      file_names = Array(path_names).map do |pn|
        Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
      end.join("\n")
      native.send_keys(file_names)
    end
  end

  def with_file_detector
    if driver.options[:browser] == :remote &&
       bridge.respond_to?(:file_detector) &&
       bridge.file_detector.nil?
      begin
        bridge.file_detector = lambda do |(fn, *)|
          str = fn.to_s
          str if File.exist?(str)
        end
        yield
      ensure
        bridge.file_detector = nil
      end
    else
      yield
    end
  end

  def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
    # Ensure we are focused on the element
    click

    editable = driver.execute_script <<-JS, self
      if (arguments[0].isContentEditable) {
        var range = document.createRange();
        var sel = window.getSelection();
        arguments[0].focus();
        range.selectNodeContents(arguments[0]);
        sel.removeAllRanges();
        sel.addRange(range);
        return true;
      }
      return false;
    JS

    # The action api has a speed problem but both chrome and firefox 58 raise errors
    # if we use the faster direct send_keys.  For now just send_keys to the element
    # we've already focused.
    # native.send_keys(value.to_s)
    browser_action.send_keys(value.to_s).perform if editable
  end

  def action_with_modifiers(click_options)
    actions = browser_action.tap do |acts|
      if click_options.coords?
        if click_options.center_offset?
          acts.move_to(native, *click_options.coords)
        else
          right_by, down_by = *click_options.coords
          size = native.size
          left_offset = (size[:width] / 2).to_i
          top_offset = (size[:height] / 2).to_i
          left = -left_offset + right_by
          top = -top_offset + down_by
          acts.move_to(native, left, top)
        end
      else
        acts.move_to(native)
      end
    end
    modifiers_down(actions, click_options.keys)
    yield actions
    modifiers_up(actions, click_options.keys)
    actions.perform
  ensure
    act = browser_action
    act.release_actions if act.respond_to?(:release_actions)
  end

  def modifiers_down(actions, keys)
    each_key(keys) { |key| actions.key_down(key) }
    actions
  end

  def modifiers_up(actions, keys)
    each_key(keys) { |key| actions.key_up(key) }
    actions
  end

  def browser
    driver.browser
  end

  def bridge
    browser.send(:bridge)
  end

  def browser_action
    browser.action
  end

  def capabilities
    browser.capabilities
  end

  def action_pause(action, duration)
    action.pause(device: action.pointer_inputs.first, duration: duration)
  end

  def normalize_keys(keys)
    keys.map do |key|
      case key
      when :ctrl then :control
      when :command, :cmd then :meta
      else
        key
      end
    end
  end

  def each_key(keys, &block)
    normalize_keys(keys).each(&block)
  end

  def find_context
    native
  end

  def build_node(native_node, initial_cache = {})
    self.class.new(driver, native_node, initial_cache)
  end

  def attrs(*attr_names)
    return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']

    driver.evaluate_script <<~JS, self, attr_names.map(&:to_s)
      (function(el, names){
        return names.map(function(name){
          return el[name]
        });
      })(arguments[0], arguments[1]);
    JS
  end

  def native_id
    # Selenium 3 -> 4 changed the return of ref
    type_or_id, id = native.ref
    id || type_or_id
  end

  def shadow_root?
    defined?(::Selenium::WebDriver::ShadowRoot) && native.is_a?(::Selenium::WebDriver::ShadowRoot)
  end

  GET_XPATH_SCRIPT = <<~'JS'
    (function(el, xml){
      var xpath = '';
    	var pos, tempitem2;

      if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
        return "(: Shadow DOM element - no XPath :)";
      };
      while(el !== xml.documentElement) {
        pos = 0;
        tempitem2 = el;
        while(tempitem2) {
          if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
            pos += 1;
          }
          tempitem2 = tempitem2.previousSibling;
        }

        if (el.namespaceURI != xml.documentElement.namespaceURI) {
          xpath = "*[local-name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
        } else {
          xpath = el.nodeName.toUpperCase()+"["+pos+"]/"+xpath;
        }

        el = el.parentNode;
      }
      xpath = '/'+xml.documentElement.nodeName.toUpperCase()+'/'+xpath;
      xpath = xpath.replace(/\/$/, '');
      return xpath;
    })(arguments[0], document)
  JS

  OBSCURED_OR_OFFSET_SCRIPT = <<~JS
    (function(el, x, y) {
      var box = el.getBoundingClientRect();
      if (x == null) x = box.width/2;
      if (y == null) y = box.height/2 ;

      var px = box.left + x,
          py = box.top + y,
          e = document.elementFromPoint(px, py);

      if (!el.contains(e))
        return true;

      return { x: px, y: py };
    })(arguments[0], arguments[1], arguments[2])
  JS

  RAPID_APPEND_TEXT = <<~JS
    (function(el, value) {
      value = el.value + value;
      if (el.maxLength && el.maxLength != -1){
        value = value.slice(0, el.maxLength);
      }
      el.value = value;
    })(arguments[0], arguments[1])
  JS

  # SettableValue encapsulates time/date field formatting
  class SettableValue
    attr_reader :value

    def initialize(value)
      @value = value
    end

    def to_s
      value.to_s
    end

    def dateable?
      !value.is_a?(String) && value.respond_to?(:to_date)
    end

    def to_date_str
      value.to_date.iso8601
    end

    def timeable?
      !value.is_a?(String) && value.respond_to?(:to_time)
    end

    def to_time_str
      value.to_time.strftime('%H:%M')
    end

    def to_datetime_str
      value.to_time.strftime('%Y-%m-%dT%H:%M')
    end
  end
  private_constant :SettableValue

  # ClickOptions encapsulates click option logic
  class ClickOptions
    attr_reader :keys, :options

    def initialize(keys, options)
      @keys = keys
      @options = options
    end

    def coords?
      options[:x] && options[:y]
    end

    def coords
      [options[:x], options[:y]]
    end

    def center_offset?
      options[:offset] == :center
    end

    def empty?
      keys.empty? && !coords? && delay.zero?
    end

    def delay
      options[:delay] || 0
    end
  end
  private_constant :ClickOptions
end

class Capybara::Selenium::Node < < Capybara::Driver::Node


def click(keys = [], **options)
    click_options = ClickOptions.new(keys, options)
    return native.click if click_options.empty?

    perform_with_options(click_options) do |action|
      target = click_options.coords? ? nil : native
      if click_options.delay.zero?
        action.click(target)
      else
        action.click_and_hold(target)
        action_pause(action, click_options.delay)
        action.release
      end
    end
  rescue StandardError => e
    if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
       e.message.include?('Other element would receive the click')
      scroll_to_center
    end

    raise e
  end


def right_click(keys = [], **options)
    click_options = ClickOptions.new(keys, options)
    perform_with_options(click_options) do |action|
      target = click_options.coords? ? nil : native
      if click_options.delay.zero?
        action.context_click(target)
      else
        action.move_to(target) if target
        action.pointer_down(:right).then do |act|
          action_pause(act, click_options.delay)
        end.pointer_up(:right)
      end
    end
  end


def scroll_to_center
    script = <<-JS
      try {
        arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
      } catch(e) {
        arguments[0].scrollIntoView(true);
      }
    JS
    begin
      driver.execute_script(script, self)
    rescue StandardError
      # Swallow error if scrollIntoView with options isn't supported
    end
  end


def sibling_index(parent, node, selector)
    siblings = parent.find_xpath(selector)
    case siblings.size
    when 0
      '[ERROR]' # IE doesn't support full XPath (namespace-uri, etc)
    when 1
      '' # index not necessary when only one matching element
    else
      idx = siblings.index(node)
      # Element may not be found in the siblings if it has gone away
      idx.nil? ? '[ERROR]' : "[#{idx + 1}]"
    end
  end


def set_text(value, clear: nil, rapid: nil, **_unused)
    value = value.to_s
    if value.empty? && clear.nil?
      native.clear
    elsif clear == :backspace
      # Clear field by sending the correct number of backspace keys.
      backspaces = [:backspace] * self.value.to_s.length
      send_keys(:end, *backspaces, value)
    elsif clear.is_a? Array
      send_keys(*clear, value)
    else
      driver.execute_script 'arguments[0].select()', self unless clear == :none
      if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
        send_keys(value[0..3])
        driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
        send_keys(value[-3..])
      else
        send_keys(value)
      end
    end
  end


def perform_with_options(click_options, &block)
    raise ArgumentError, 'A block must be provided' unless block

    scroll_if_needed do
      action_with_modifiers(click_options) do |action|
        if block
          yield action
        else
          click_options.coords? ? action.click : action.click(native)
        end
      end
    end
  end


def update_value_js(value)
    driver.execute_script(<<-JS, self, value)
      if (arguments[0].readOnly) { return };
      if (document.activeElement !== arguments[0]){
        arguments[0].focus();
      }
      if (arguments[0].value != arguments[1]) {
        arguments[0].value = arguments[1]
        arguments[0].dispatchEvent(new InputEvent('input'));
        arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
      }
    JS
  end


def with_file_detector
    if driver.options[:browser] == :remote &&
       bridge.respond_to?(:file_detector) &&
       bridge.file_detector.nil?
      begin
        bridge.file_detector = lambda do |(fn, *)|
          str = fn.to_s
          str if File.exist?(str)
        end
        yield
      ensure
        bridge.file_detector = nil
      end
    else
      yield
    end
  end


def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
    # Ensure we are focused on the element
    click

    editable = driver.execute_script <<-JS, self
      if (arguments[0].isContentEditable) {
        var range = document.createRange();
        var sel = window.getSelection();
        arguments[0].focus();
        range.selectNodeContents(arguments[0]);
        sel.removeAllRanges();
        sel.addRange(range);
        return true;
      }
      return false;
    JS

    # The action api has a speed problem but both chrome and firefox 58 raise errors
    # if we use the faster direct send_keys.  For now just send_keys to the element
    # we've already focused.
    # native.send_keys(value.to_s)
    browser_action.send_keys(value.to_s).perform if editable
  end


def action_with_modifiers(click_options)
    actions = browser_action.tap do |acts|
      if click_options.coords?
        if click_options.center_offset?
          acts.move_to(native, *click_options.coords)
        else
          right_by, down_by = *click_options.coords
          size = native.size
          left_offset = (size[:width] / 2).to_i
          top_offset = (size[:height] / 2).to_i
          left = -left_offset + right_by
          top = -top_offset + down_by
          acts.move_to(native, left, top)
        end
      else
        acts.move_to(native)
      end
    end
    modifiers_down(actions, click_options.keys)
    yield actions
    modifiers_up(actions, click_options.keys)
    actions.perform
  ensure
    act = browser_action
    act.release_actions if act.respond_to?(:release_actions)
  end


def normalize_keys(keys)
    keys.map do |key|
      case key
      when :ctrl then :control
      when :command, :cmd then :meta
      else
        key
      end
    end
  end


def attrs(*attr_names)
    return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']

    driver.evaluate_script <<~JS, self, attr_names.map(&:to_s)
      (function(el, names){
        return names.map(function(name){
          return el[name]
        });
      })(arguments[0], arguments[1]);
    JS
  end


# frozen_string_literal: true

module Capybara
  class Server
    class AnimationDisabler
      def self.selector_for(css_or_bool)
        case css_or_bool
        when String
          css_or_bool
        when true
          '*'
        else
          raise CapybaraError, 'Capybara.disable_animation supports either a String (the css selector to disable) or a boolean'
        end
      end

      def initialize(app)
        @app = app
        @disable_css_markup = format(DISABLE_CSS_MARKUP_TEMPLATE,
                                     selector: self.class.selector_for(Capybara.disable_animation))
        @disable_js_markup = +DISABLE_JS_MARKUP_TEMPLATE
      end

      def call(env)
        status, headers, body = @app.call(env)
        return [status, headers, body] unless html_content?(headers)

        nonces = directive_nonces(headers).transform_values { |nonce| "nonce=\"#{nonce}\"" if nonce && !nonce.empty? }
        response = Rack::Response.new([], status, headers)

        body.each { |html| response.write insert_disable(html, nonces) }
        body.close if body.respond_to?(:close)

        response.finish
      end

    private

      attr_reader :disable_css_markup, :disable_js_markup

      def html_content?(headers)
        /html/.match?(headers['Content-Type']) # rubocop:todo Performance/StringInclude
      end

      def insert_disable(html, nonces)
        html.sub(%r{()}, "\\1")
            .sub(%r{()}, "\\1")
      end

      def directive_nonces(headers)
        headers.fetch('Content-Security-Policy', '')
               .split(';')
               .map(&:split)
               .to_h do |s|
                 [
                   s[0], s[1..].filter_map do |value|
                     /^'nonce-(?<nonce>.+)'/ =~ value
                     nonce
                   end[0]
                 ]
               end
      end

      DISABLE_CSS_MARKUP_TEMPLATE = <<~CSS
        %<selector>s, %<selector>s::before, %<selector>s::after {
           transition: none !important;
           animation-duration: 0s !important;
           animation-delay: 0s !important;
           scroll-behavior: auto !important;
        }
      CSS

      DISABLE_JS_MARKUP_TEMPLATE = <<~SCRIPT
        //<![CDATA[
          (typeof jQuery !== 'undefined') && (jQuery.fx.off = true);
        //]]>
      SCRIPT
    end
  end
end


def directive_nonces(headers)
        headers.fetch('Content-Security-Policy', '')
               .split(';')
               .map(&:split)
               .to_h do |s|
                 [
                   s[0], s[1..].filter_map do |value|
                     /^'nonce-(?<nonce>.+)'/ =~ value
                     nonce
                   end[0]
                 ]
               end
      end


# frozen_string_literal: true

module Capybara
  class Server
    class Middleware
      class Counter
        def initialize
          @value = []
          @mutex = Mutex.new
        end

        def increment(uri)
          @mutex.synchronize { @value.push(uri) }
        end

        def decrement(uri)
          @mutex.synchronize { @value.delete_at(@value.index(uri) || - 1) }
        end

        def positive?
          @mutex.synchronize { @value.length.positive? }
        end

        def value
          @mutex.synchronize { @value.dup }
        end
      end

      attr_reader :error

      def initialize(app, server_errors, extra_middleware = [])
        @app = app
        @extended_app = extra_middleware.inject(@app) do |ex_app, klass|
          klass.new(ex_app)
        end
        @counter = Counter.new
        @server_errors = server_errors
      end

      def pending_requests
        @counter.value
      end

      def pending_requests?
        @counter.positive?
      end

      def clear_error
        @error = nil
      end

      def call(env)
        if env['PATH_INFO'] == '/__identify__'
          [200, {}, [@app.object_id.to_s]]
        else
          request_uri = env['REQUEST_URI']
          @counter.increment(request_uri)

          begin
            @extended_app.call(env)
          rescue *@server_errors => e
            @error ||= e
            raise e
          ensure
            @counter.decrement(request_uri)
          end
        end
      end
    end
  end
end


# frozen_string_literal: true

module Puma
  module MiniSSL
    class Socket
      def read_nonblock(size, *_)
        wait_states = %i[wait_readable wait_writable]

        loop do
          output = engine_read_all
          return output if output

          data = @socket.read_nonblock(size, exception: false)
          raise IO::EAGAINWaitReadable if wait_states.include? data
          return nil if data.nil?

          @engine.inject(data)
          output = engine_read_all

          return output if output

          while (neg_data = @engine.extract)
            @socket.write neg_data
          end
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'capybara/rspec/matchers/compound'
require 'capybara/rspec/matchers/count_sugar'
require 'capybara/rspec/matchers/spatial_sugar'

module Capybara
  module RSpecMatchers
    module Matchers
      class Base
        include ::Capybara::RSpecMatchers::Matchers::Compound if defined?(::Capybara::RSpecMatchers::Matchers::Compound)

        attr_reader :failure_message, :failure_message_when_negated

        def initialize(*args, **kw_args, &filter_block)
          @args = args.dup
          @kw_args = kw_args || {}
          @filter_block = filter_block
        end

      private

        def session_query_args
          # if @args.last.is_a? Hash
          #   @args.last[:session_options] = session_options
          # else
          #   @args.push(session_options: session_options)
          # end
          @args
        end

        def session_query_options
          @kw_args[:session_options] = session_options
          @kw_args
        end

        def session_options
          @context_el ||= nil
          if @context_el.respond_to? :session_options
            @context_el.session_options
          elsif @context_el.respond_to? :current_scope
            @context_el.current_scope.session_options
          else
            Capybara.session_options
          end
        end
      end

      class WrappedElementMatcher < Base
        def matches?(actual, &filter_block)
          @filter_block ||= filter_block
          element_matches?(wrap(actual))
        rescue Capybara::ExpectationNotMet => e
          @failure_message = e.message
          false
        end

        def does_not_match?(actual, &filter_block)
          @filter_block ||= filter_block
          element_does_not_match?(wrap(actual))
        rescue Capybara::ExpectationNotMet => e
          @failure_message_when_negated = e.message
          false
        end

      private

        def wrap(actual)
          actual = actual.to_capybara_node if actual.respond_to?(:to_capybara_node)
          @context_el = if actual.respond_to?(:has_selector?)
            actual
          else
            Capybara.string(actual.to_s)
          end
        end
      end

      class CountableWrappedElementMatcher < WrappedElementMatcher
        include ::Capybara::RSpecMatchers::CountSugar
        include ::Capybara::RSpecMatchers::SpatialSugar
      end

      class NegatedMatcher
        include ::Capybara::RSpecMatchers::Matchers::Compound if defined?(::Capybara::RSpecMatchers::Matchers::Compound)

        def initialize(matcher)
          super()
          @matcher = matcher
        end

        def matches?(actual, &filter_block)
          @matcher.does_not_match?(actual, &filter_block)
        end

        def does_not_match?(actual, &filter_block)
          @matcher.matches?(actual, &filter_block)
        end

        def description
          "not #{@matcher.description}"
        end

        def failure_message
          @matcher.failure_message_when_negated
        end

        def failure_message_when_negated
          @matcher.failure_message
        end
      end
    end
  end
end


# frozen_string_literal: true

module Capybara
  module RSpecMatchers
    module Matchers
      class BecomeClosed
        def initialize(options)
          @options = options
        end

        def matches?(window)
          @window = window
          @wait_time = Capybara::Queries::BaseQuery.wait(@options, window.session.config.default_max_wait_time)
          timer = Capybara::Helpers.timer(expire_in: @wait_time)
          while window.exists?
            return false if timer.expired?

            sleep 0.01
          end
          true
        end

        def failure_message
          "expected #{@window.inspect} to become closed after #{@wait_time} seconds"
        end

        def failure_message_when_negated
          "expected #{@window.inspect} not to become closed after #{@wait_time} seconds"
        end
      end
    end
  end
end


# frozen_string_literal: true

if defined?(RSpec::Expectations::Version)
  module Capybara
    module RSpecMatchers
      module Matchers
        module Compound
          include ::RSpec::Matchers::Composable

          def and(matcher)
            And.new(self, matcher)
          end

          def and_then(matcher)
            ::RSpec::Matchers::BuiltIn::Compound::And.new(self, matcher)
          end

          def or(matcher)
            Or.new(self, matcher)
          end

          class CapybaraEvaluator
            def initialize(actual)
              @actual = actual
              @match_results = Hash.new { |hsh, matcher| hsh[matcher] = matcher.matches?(@actual) }
            end

            def matcher_matches?(matcher)
              @match_results[matcher]
            end

            def reset
              @match_results.clear
            end
          end

          # @api private
          module Synchronizer
            def match(_expected, actual)
              @evaluator = CapybaraEvaluator.new(actual)
              syncer = sync_element(actual)
              begin
                syncer.synchronize do
                  @evaluator.reset
                  raise ::Capybara::ElementNotFound unless synchronized_match?

                  true
                end
              rescue StandardError
                false
              end
            end

            def sync_element(el)
              if el.respond_to? :synchronize
                el
              elsif el.respond_to? :current_scope
                el.current_scope
              else
                Capybara.string(el)
              end
            end
          end

          class And < ::RSpec::Matchers::BuiltIn::Compound::And
            include Synchronizer

          private

            def synchronized_match?
              [matcher_1_matches?, matcher_2_matches?].all?
            end
          end

          class Or < ::RSpec::Matchers::BuiltIn::Compound::Or
            include Synchronizer

          private

            def synchronized_match?
              [matcher_1_matches?, matcher_2_matches?].any?
            end
          end
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'xpath'

module Capybara
  class Selector
    # @api private
    class CSSBuilder
      def initialize(expression)
        @expression = expression || ''
      end

      attr_reader :expression

      def add_attribute_conditions(**attributes)
        @expression = attributes.inject(expression) do |css, (name, value)|
          conditions = if name == :class
            class_conditions(value)
          elsif value.is_a? Regexp
            regexp_conditions(name, value)
          else
            [attribute_conditions(name => value)]
          end

          ::Capybara::Selector::CSS.split(css).map do |sel|
            next sel if conditions.empty?

            conditions.map { |cond| sel + cond }.join(', ')
          end.join(', ')
        end
      end

    private

      def regexp_conditions(name, value)
        Selector::RegexpDisassembler.new(value).alternated_substrings.map do |strs|
          strs.map do |str|
            "[#{name}*='#{str}'#{' i' if value.casefold?}]"
          end.join
        end
      end

      def attribute_conditions(attributes)
        attributes.map do |attribute, value|
          case value
          when XPath::Expression
            raise ArgumentError, "XPath expressions are not supported for the :#{attribute} filter with CSS based selectors"
          when Regexp
            Selector::RegexpDisassembler.new(value).substrings.map do |str|
              "[#{attribute}*='#{str}'#{' i' if value.casefold?}]"
            end.join
          when true
            "[#{attribute}]"
          when false
            ':not([attribute])'
          else
            if attribute == :id
              "##{::Capybara::Selector::CSS.escape(value)}"
            else
              "[#{attribute}='#{value}']"
            end
          end
        end.join
      end

      def class_conditions(classes)
        case classes
        when XPath::Expression
          raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
        when Regexp
          Selector::RegexpDisassembler.new(classes).alternated_substrings.map do |strs|
            strs.map do |str|
              "[class*='#{str}'#{' i' if classes.casefold?}]"
            end.join
          end
        else
          cls = Array(classes).reject { |c| c.is_a? Regexp }.group_by { |cl| cl.match?(/^!(?!!!)/) }
          [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
          cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..))})" }).join]
        end
      end
    end
  end
end


def attribute_conditions(attributes)
        attributes.map do |attribute, value|
          case value
          when XPath::Expression
            raise ArgumentError, "XPath expressions are not supported for the :#{attribute} filter with CSS based selectors"
          when Regexp
            Selector::RegexpDisassembler.new(value).substrings.map do |str|
              "[#{attribute}*='#{str}'#{' i' if value.casefold?}]"
            end.join
          when true
            "[#{attribute}]"
          when false
            ':not([attribute])'
          else
            if attribute == :id
              "##{::Capybara::Selector::CSS.escape(value)}"
            else
              "[#{attribute}='#{value}']"
            end
          end
        end.join
      end


def class_conditions(classes)
        case classes
        when XPath::Expression
          raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
        when Regexp
          Selector::RegexpDisassembler.new(classes).alternated_substrings.map do |strs|
            strs.map do |str|
              "[class*='#{str}'#{' i' if classes.casefold?}]"
            end.join
          end
        else
          cls = Array(classes).reject { |c| c.is_a? Regexp }.group_by { |cl| cl.match?(/^!(?!!!)/) }
          [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
          cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..))})" }).join]
        end
      end


# frozen_string_literal: true

require 'xpath'

module Capybara
  class Selector
    # @api private
    class XPathBuilder
      def initialize(expression)
        @expression = expression || ''
      end

      attr_reader :expression

      def add_attribute_conditions(**conditions)
        @expression = conditions.inject(expression) do |xp, (name, value)|
          conditions = name == :class ? class_conditions(value) : attribute_conditions(name => value)
          return xp if conditions.nil?

          if xp.is_a? XPath::Expression
            xp[conditions]
          else
            "(#{xp})[#{conditions}]"
          end
        end
      end

    private

      def attribute_conditions(attributes)
        attributes.map do |attribute, value|
          case value
          when XPath::Expression
            XPath.attr(attribute)[value]
          when Regexp
            XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
          when true
            XPath.attr(attribute)
          when false, nil
            !XPath.attr(attribute)
          else
            XPath.attr(attribute) == value.to_s
          end
        end.reduce(:&)
      end

      def class_conditions(classes)
        case classes
        when XPath::Expression, Regexp
          attribute_conditions(class: classes)
        else
          Array(classes).reject { |c| c.is_a? Regexp }.map do |klass|
            if klass.match?(/^!(?!!!)/)
              !XPath.attr(:class).contains_word(klass.slice(1..))
            else
              XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
            end
          end.reduce(:&)
        end
      end

      def regexp_to_xpath_conditions(regexp)
        condition = XPath.current
        condition = condition.uppercase if regexp.casefold?
        Selector::RegexpDisassembler.new(regexp).alternated_substrings.map do |strs|
          strs.map { |str| condition.contains(str) }.reduce(:&)
        end.reduce(:|)
      end
    end
  end
end


def attribute_conditions(attributes)
        attributes.map do |attribute, value|
          case value
          when XPath::Expression
            XPath.attr(attribute)[value]
          when Regexp
            XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
          when true
            XPath.attr(attribute)
          when false, nil
            !XPath.attr(attribute)
          else
            XPath.attr(attribute) == value.to_s
          end
        end.reduce(:&)
      end


def class_conditions(classes)
        case classes
        when XPath::Expression, Regexp
          attribute_conditions(class: classes)
        else
          Array(classes).reject { |c| c.is_a? Regexp }.map do |klass|
            if klass.match?(/^!(?!!!)/)
              !XPath.attr(:class).contains_word(klass.slice(1..))
            else
              XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
            end
          end.reduce(:&)
        end
      end


# frozen_string_literal: true

Capybara.add_selector(:button, locator_type: [String, Symbol]) do
  xpath(:value, :title, :type, :name) do |locator, **options|
    input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
    btn_xpath = XPath.descendant(:button)
    aria_btn_xpath = XPath.descendant[XPath.attr(:role).equals('button')]
    image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']

    unless locator.nil?
      locator = locator.to_s
      locator_matchers = combine_locators(locator, config: self)
      btn_matchers = locator_matchers |
                     XPath.string.n.is(locator) |
                     XPath.descendant(:img)[XPath.attr(:alt).is(locator)]

      label_contains_xpath = locate_label(locator).descendant[labellable_elements]
      input_btn_xpath = input_btn_xpath[locator_matchers]
      btn_xpath = btn_xpath[btn_matchers]
      aria_btn_xpath = aria_btn_xpath[btn_matchers]

      alt_matches = XPath.attr(:alt).is(locator)
      alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
      image_btn_xpath = image_btn_xpath[alt_matches]
    end

    btn_xpaths = [input_btn_xpath, btn_xpath, image_btn_xpath, label_contains_xpath].compact
    btn_xpaths << aria_btn_xpath if enable_aria_role

    %i[value title type].inject(btn_xpaths.inject(&:union)) do |memo, ef|
      memo.where(find_by_attr(ef, options[ef]))
    end
  end

  node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
  expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }

  node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
  expression_filter(:name) do |xpath, val|
    builder(xpath).add_attribute_conditions(name: val)
  end

  describe_expression_filters do |disabled: nil, **options|
    desc = +''
    desc << ' that is not disabled' if disabled == false
    desc << describe_all_expression_filters(**options)
  end

  describe_node_filters do |disabled: nil, **|
    ' that is disabled' if disabled == true
  end

  def combine_locators(locator, config:)
    [
      XPath.attr(:id).equals(locator),
      XPath.attr(:name).equals(locator),
      XPath.attr(:value).is(locator),
      XPath.attr(:title).is(locator),
      (XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)),
      (XPath.attr(:'aria-label').is(locator) if config.enable_aria_label),
      (XPath.attr(config.test_id) == locator if config.test_id)
    ].compact.inject(&:|)
  end

  def labellable_elements
    (XPath.self(:input) & XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')) | XPath.self(:button)
  end
end


# frozen_string_literal: true

Capybara.add_selector(:table, locator_type: [String, Symbol]) do
  xpath do |locator, caption: nil, **|
    xpath = XPath.descendant(:table)
    unless locator.nil?
      locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)
      locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
      xpath = xpath[locator_matchers]
    end
    xpath = xpath[XPath.descendant(:caption) == caption] if caption
    xpath
  end

  expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols|
    col_conditions = cols.map do |col|
      if col.is_a? Hash
        col.reduce(nil) do |xp, (header, cell_str)|
          header = XPath.descendant(:th)[XPath.string.n.is(header)]
          td = XPath.descendant(:tr)[header].descendant(:td)
          cell_condition = XPath.string.n.is(cell_str)
          if xp
            prev_cell = XPath.ancestor(:table)[1].join(xp)
            cell_condition &= (prev_cell & prev_col_position?(prev_cell))
          end
          td[cell_condition]
        end
      else
        cells_xp = col.reduce(nil) do |prev_cell, cell_str|
          cell_condition = XPath.string.n.is(cell_str)

          if prev_cell
            prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell)
            cell_condition &= (prev_cell & prev_col_position?(prev_cell))
          end

          XPath.descendant(:td)[cell_condition]
        end
        XPath.descendant(:tr).join(cells_xp)
      end
    end.reduce(:&)
    xpath[col_conditions]
  end

  expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
    raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all?(Array)

    rows = cols.transpose
    col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
    xpath[match_row_count(rows.size)][col_conditions]
  end

  expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
    rows_conditions = rows.map { |row| match_row(row) }.reduce(:&)
    xpath[rows_conditions]
  end

  expression_filter(:rows, valid_values: [Array]) do |xpath, rows|
    rows_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
    xpath[match_row_count(rows.size)][rows_conditions]
  end

  describe_expression_filters do |caption: nil, **|
    " with caption \"#{caption}\"" if caption
  end

  def prev_col_position?(cell)
    XPath.position.equals(cell_position(cell))
  end

  def cell_position(cell)
    cell.preceding_sibling(:td).count.plus(1)
  end

  def match_row(row, match_size: false)
    xp = XPath.descendant(:tr)[
      if row.is_a? Hash
        row_match_cells_to_headers(row)
      else
        XPath.descendant(:td)[row_match_ordered_cells(row)]
      end
    ]
    xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size
    xp
  end

  def match_row_count(size)
    XPath.descendant(:tbody).descendant(:tr).count.equals(size) |
      (XPath.descendant(:tr).count.equals(size) & ~XPath.descendant(:tbody))
  end

  def row_match_cells_to_headers(row)
    row.map do |header, cell|
      header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
      XPath.descendant(:td)[
        XPath.string.n.is(cell) & header_xp.boolean & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
      ]
    end.reduce(:&)
  end

  def row_match_ordered_cells(row)
    row_conditions = row.map do |cell|
      XPath.self(:td)[XPath.string.n.is(cell)]
    end
    row_conditions.reverse.reduce do |cond, cell|
      cell[XPath.following_sibling[cond]]
    end
  end
end


# frozen_string_literal: true

require 'capybara/selector/filters/base'

module Capybara
  class Selector
    module Filters
      class NodeFilter < Base
        def initialize(name, matcher, block, **options)
          super
          @block = if boolean?
            proc do |node, value|
              error_cnt = errors.size
              block.call(node, value).tap do |res|
                add_error("Expected #{name} #{value} but it wasn't") if !res && error_cnt == errors.size
              end
            end
          else
            block
          end
        end

        def matches?(node, name, value, context = nil)
          apply(node, name, value, true, context)
        rescue Capybara::ElementNotFound
          false
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'capybara/selenium/nodes/chrome_node'
require 'capybara/selenium/patches/logs'

module Capybara::Selenium::Driver::ChromeDriver
  def self.extended(base)
    bridge = base.send(:bridge)
    bridge.extend Capybara::Selenium::ChromeLogs unless bridge.respond_to?(:log)
    bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
    base.options[:native_displayed] = false if base.options[:native_displayed].nil?
  end

  def fullscreen_window(handle)
    within_given_window(handle) do
      super
    rescue NoMethodError => e
      raise unless e.message.include?('full_screen_window')

      result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
      result['value']
    end
  end

  def resize_window_to(handle, width, height)
    super
  rescue Selenium::WebDriver::Error::UnknownError => e
    raise unless e.message.include?('failed to change window state')

    # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
    # and raises unnecessary error. Wait a bit and try again.
    sleep 0.25
    super
  end

  def reset!
    # Use instance variable directly so we avoid starting the browser just to reset the session
    return unless @browser

    switch_to_window(window_handles.first)
    window_handles.slice(1..).each { |win| close_window(win) }
    return super if chromedriver_version < 73

    timer = Capybara::Helpers.timer(expire_in: 10)
    begin
      clear_storage unless uniform_storage_clear?
      @browser.navigate.to('about:blank')
      wait_for_empty_page(timer)
    rescue *unhandled_alert_errors
      accept_unhandled_reset_alert
      retry
    end

    execute_cdp('Storage.clearDataForOrigin', origin: '*', storageTypes: storage_types_to_clear)
  end

private

  def storage_types_to_clear
    types = ['cookies']
    types << 'local_storage' if clear_all_storage?
    types.join(',')
  end

  def clear_all_storage?
    storage_clears.none? false
  end

  def uniform_storage_clear?
    storage_clears.uniq { |s| s == false }.length <= 1
  end

  def storage_clears
    options.values_at(:clear_session_storage, :clear_local_storage)
  end

  def clear_storage
    # Chrome errors if attempt to clear storage on about:blank
    # In W3C mode it crashes chromedriver
    url = current_url
    super unless url.nil? || url.start_with?('about:')
  end

  def delete_all_cookies
    execute_cdp('Network.clearBrowserCookies')
  rescue *cdp_unsupported_errors
    # If the CDP clear isn't supported do original limited clear
    super
  end

  def cdp_unsupported_errors
    @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError]
  end

  def execute_cdp(cmd, params = {})
    if browser.respond_to? :execute_cdp
      browser.execute_cdp(cmd, **params)
    else
      args = { cmd: cmd, params: params }
      result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
      result['value']
    end
  end

  def build_node(native_node, initial_cache = {})
    ::Capybara::Selenium::ChromeNode.new(self, native_node, initial_cache)
  end

  def chromedriver_version
    @chromedriver_version ||= begin
      caps = browser.capabilities
      caps['chrome']&.fetch('chromedriverVersion', nil).to_f
    end
  end
end

Capybara::Selenium::Driver.register_specialization :chrome, Capybara::Selenium::Driver::ChromeDriver


# frozen_string_literal: true

require 'capybara/selenium/nodes/edge_node'

module Capybara::Selenium::Driver::EdgeDriver
  def self.extended(base)
    bridge = base.send(:bridge)
    bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
    base.options[:native_displayed] = false if base.options[:native_displayed].nil?
  end

  def fullscreen_window(handle)
    return super if edgedriver_version < 75

    within_given_window(handle) do
      super
    rescue NoMethodError => e
      raise unless e.message.include?('full_screen_window')

      result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
      result['value']
    end
  end

  def resize_window_to(handle, width, height)
    super
  rescue Selenium::WebDriver::Error::UnknownError => e
    raise unless e.message.include?('failed to change window state')

    # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
    # and raises unnecessary error. Wait a bit and try again.
    sleep 0.25
    super
  end

  def reset!
    return super if edgedriver_version < 75
    # Use instance variable directly so we avoid starting the browser just to reset the session
    return unless @browser

    switch_to_window(window_handles.first)
    window_handles.slice(1..).each { |win| close_window(win) }

    timer = Capybara::Helpers.timer(expire_in: 10)
    begin
      clear_storage unless uniform_storage_clear?
      @browser.navigate.to('about:blank')
      wait_for_empty_page(timer)
    rescue *unhandled_alert_errors
      accept_unhandled_reset_alert
      retry
    end

    execute_cdp('Storage.clearDataForOrigin', origin: '*', storageTypes: storage_types_to_clear)
  end

  def download_path=(path)
    if @browser.respond_to?(:download_path=)
      @browser.download_path = path
    else
      # Not yet implemented in seleniun-webdriver for edge so do it ourselves
      execute_cdp('Page.setDownloadBehavior', behavior: 'allow', downloadPath: path)
    end
  end

private

  def storage_types_to_clear
    types = ['cookies']
    types << 'local_storage' if clear_all_storage?
    types.join(',')
  end

  def clear_all_storage?
    storage_clears.none? false
  end

  def uniform_storage_clear?
    storage_clears.uniq { |s| s == false }.length <= 1
  end

  def storage_clears
    options.values_at(:clear_session_storage, :clear_local_storage)
  end

  def clear_storage
    # Edgedriver crashes if attempt to clear storage on about:blank
    url = current_url
    super unless url.nil? || url.start_with?('about:')
  end

  def delete_all_cookies
    return super if edgedriver_version < 75

    execute_cdp('Network.clearBrowserCookies')
  rescue *cdp_unsupported_errors
    # If the CDP clear isn't supported do original limited clear
    super
  end

  def cdp_unsupported_errors
    @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError]
  end

  def execute_cdp(cmd, params = {})
    if browser.respond_to? :execute_cdp
      browser.execute_cdp(cmd, **params)
    else
      args = { cmd: cmd, params: params }
      result = bridge.http.call(:post, "session/#{bridge.session_id}/ms/cdp/execute", args)
      result['value']
    end
  end

  def build_node(native_node, initial_cache = {})
    ::Capybara::Selenium::EdgeNode.new(self, native_node, initial_cache)
  end

  def edgedriver_version
    @edgedriver_version ||= begin
      caps = browser.capabilities
      caps['msedge']&.fetch('msedgedriverVersion', nil).to_f
    end
  end
end

Capybara::Selenium::Driver.register_specialization :edge, Capybara::Selenium::Driver::EdgeDriver
Capybara::Selenium::Driver.register_specialization :edge_chrome, Capybara::Selenium::Driver::EdgeDriver


def reset!
    return super if edgedriver_version < 75
    # Use instance variable directly so we avoid starting the browser just to reset the session
    return unless @browser

    switch_to_window(window_handles.first)
    window_handles.slice(1..).each { |win| close_window(win) }

    timer = Capybara::Helpers.timer(expire_in: 10)
    begin
      clear_storage unless uniform_storage_clear?
      @browser.navigate.to('about:blank')
      wait_for_empty_page(timer)
    rescue *unhandled_alert_errors
      accept_unhandled_reset_alert
      retry
    end

    execute_cdp('Storage.clearDataForOrigin', origin: '*', storageTypes: storage_types_to_clear)
  end


# frozen_string_literal: true

require 'capybara/selenium/nodes/firefox_node'

module Capybara::Selenium::Driver::FirefoxDriver
  def self.extended(driver)
    driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver
    bridge = driver.send(:bridge)
    bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
  end
end

module Capybara::Selenium::Driver::W3CFirefoxDriver
  class << self
    def extended(driver)
      require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(driver.browser)
      driver.options[:native_displayed] = false if driver.options[:native_displayed].nil?
    end

    def pause_broken?(sel_driver)
      sel_driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
    end
  end

  def resize_window_to(handle, width, height)
    within_given_window(handle) do
      # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
      if window_size(handle) == [width, height]
        {}
      else
        super
      end
    end
  end

  def reset!
    # Use instance variable directly so we avoid starting the browser just to reset the session
    return unless @browser

    if browser_version >= 68
      begin
        # Firefox 68 hangs if we try to switch windows while a modal is visible
        browser.switch_to.alert&.dismiss
      rescue Selenium::WebDriver::Error::NoSuchAlertError
        # Swallow
      end
    end

    switch_to_window(window_handles.first)
    window_handles.slice(1..).each { |win| close_window(win) }
    super
  end

  def refresh
    # Accept any "will repost content" confirmation that occurs
    accept_modal :confirm, wait: 0.1 do
      super
    end
  rescue Capybara::ModalNotFound
    # No modal was opened - page has refreshed - ignore
  end

  def switch_to_frame(frame)
    return super unless frame == :parent

    # geckodriver/firefox has an issue if the current frame is removed from within it
    # so we have to move to the default_content and iterate back through the frames
    handles = @frame_handles[current_window_handle]
    browser.switch_to.default_content
    handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
  end

private

  def build_node(native_node, initial_cache = {})
    ::Capybara::Selenium::FirefoxNode.new(self, native_node, initial_cache)
  end

  def browser_version
    browser.capabilities[:browser_version].to_f
  end
end

Capybara::Selenium::Driver.register_specialization :firefox, Capybara::Selenium::Driver::FirefoxDriver


# frozen_string_literal: true

class Capybara::Selenium::Node
  module FileInputClickEmulation
    def click(keys = [], **options)
      super
    rescue Selenium::WebDriver::Error::InvalidArgumentError
      return emulate_click if attaching_file? && visible_file_field?

      raise
    end

  private

    def visible_file_field?
      (attrs(:tagName, :type).map { |val| val&.downcase } == %w[input file]) && visible?
    end

    def attaching_file?
      caller_locations.any? { |cl| cl.base_label == 'attach_file' }
    end

    def emulate_click
      driver.execute_script(<<~JS, self)
        arguments[0].dispatchEvent(
          new MouseEvent('click', {
            view: window,
            bubbles: true,
            cancelable: true
          }));
      JS
    end
  end
end


# frozen_string_literal: true

module Capybara
  module Selenium
    module Find
      def find_xpath(selector, uses_visibility: false, styles: nil, position: false, **_options)
        find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles, position: position)
      end

      def find_css(selector, uses_visibility: false, texts: [], styles: nil, position: false, **_options)
        find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles, position: position)
      end

    private

      def find_by(format, selector, uses_visibility:, texts:, styles:, position:)
        els = find_context.find_elements(format, selector)
        hints = []

        if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
          els = filter_by_text(els, texts) unless texts.empty?
          hints = begin
            gather_hints(els, uses_visibility: uses_visibility, styles: styles, position: position)
          rescue Selenium::WebDriver::Error::JavascriptError
            # Unclear how this can happen but issue #2729 indicates it can
            []
          end
        end
        els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
      end

      def gather_hints(elements, uses_visibility:, styles:, position:)
        hints_js, functions = build_hints_js(uses_visibility, styles, position)
        return [] unless functions.any?

        (es_context.execute_script(hints_js, elements) || []).map! do |results|
          hint = {}
          hint[:style] = results.pop if functions.include?(:style_func)
          hint[:position] = results.pop if functions.include?(:position_func)
          hint[:visible] = results.pop if functions.include?(:vis_func)
          hint
        end
      rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
             ::Capybara::NotSupportedByDriverError
        # warn 'Unexpected Stale Element Error - skipping optimization'
        []
      end

      def filter_by_text(elements, texts)
        es_context.execute_script <<~JS, elements, texts
          var texts = arguments[1];
          return arguments[0].filter(function(el){
            var content = el.textContent.toLowerCase();
            return texts.every(function(txt){ return content.indexOf(txt.toLowerCase()) != -1 });
          })
        JS
      end

      def build_hints_js(uses_visibility, styles, position)
        functions = []
        hints_js = +''

        if uses_visibility && !is_displayed_atom.empty?
          hints_js << <<~VISIBILITY_JS
            var vis_func = #{is_displayed_atom};
          VISIBILITY_JS
          functions << :vis_func
        end

        if position
          hints_js << <<~POSITION_JS
            var position_func = function(el){
              return el.getBoundingClientRect();
            };
          POSITION_JS
          functions << :position_func
        end

        if styles.is_a? Hash
          hints_js << <<~STYLE_JS
            var style_func = function(el){
              var el_styles = window.getComputedStyle(el);
              return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
                res[style] = el_styles[style];
                return res;
              }, {});
            };
          STYLE_JS
          functions << :style_func
        end

        hints_js << <<~EACH_JS
          return arguments[0].map(function(el){
            return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
          });
        EACH_JS

        [hints_js, functions]
      end

      def es_context
        respond_to?(:execute_script) ? self : driver
      end

      def is_displayed_atom # rubocop:disable Naming/PredicatePrefix
        @@is_displayed_atom ||= begin # rubocop:disable Style/ClassVars
          browser.send(:bridge).send(:read_atom, 'isDisplayed')
        rescue StandardError
          # If the atom doesn't exist or other error
          ''
        end
      end
    end
  end
end


def gather_hints(elements, uses_visibility:, styles:, position:)
        hints_js, functions = build_hints_js(uses_visibility, styles, position)
        return [] unless functions.any?

        (es_context.execute_script(hints_js, elements) || []).map! do |results|
          hint = {}
          hint[:style] = results.pop if functions.include?(:style_func)
          hint[:position] = results.pop if functions.include?(:position_func)
          hint[:visible] = results.pop if functions.include?(:vis_func)
          hint
        end
      rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
             ::Capybara::NotSupportedByDriverError
        # warn 'Unexpected Stale Element Error - skipping optimization'
        []
      end


def build_hints_js(uses_visibility, styles, position)
        functions = []
        hints_js = +''

        if uses_visibility && !is_displayed_atom.empty?
          hints_js << <<~VISIBILITY_JS
            var vis_func = #{is_displayed_atom};
          VISIBILITY_JS
          functions << :vis_func
        end

        if position
          hints_js << <<~POSITION_JS
            var position_func = function(el){
              return el.getBoundingClientRect();
            };
          POSITION_JS
          functions << :position_func
        end

        if styles.is_a? Hash
          hints_js << <<~STYLE_JS
            var style_func = function(el){
              var el_styles = window.getComputedStyle(el);
              return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
                res[style] = el_styles[style];
                return res;
              }, {});
            };
          STYLE_JS
          functions << :style_func
        end

        hints_js << <<~EACH_JS
          return arguments[0].map(function(el){
            return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
          });
        EACH_JS

        [hints_js, functions]
      end


# frozen_string_literal: true

class Capybara::Selenium::Node
  module Html5Drag
    # Implement methods to emulate HTML5 drag and drop

    def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
      drop_modifiers = Array(drop_modifiers)

      driver.execute_script MOUSEDOWN_TRACKER
      scroll_if_needed { browser_action.click_and_hold(native).perform }
      html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
      if html5
        perform_html5_drag(element, delay, drop_modifiers)
      else
        perform_legacy_drag(element, drop_modifiers)
      end
    end

  private

    def perform_legacy_drag(element, drop_modifiers)
      element.scroll_if_needed do
        # browser_action.move_to(element.native).release.perform
        keys_down = modifiers_down(browser_action, drop_modifiers)
        keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
        keys_up.perform
      end
    end

    def perform_html5_drag(element, delay, drop_modifiers)
      driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000, normalize_keys(drop_modifiers)
      browser_action.release.perform
    end

    def html5_drop(*args)
      if args[0].is_a? String
        input = driver.evaluate_script ATTACH_FILE
        input.set_file(args)
        driver.execute_script DROP_FILE, self, input
      else
        items = args.flat_map do |arg|
          arg.map { |(type, data)| { type: type, data: data } }
        end
        driver.execute_script DROP_STRING, items, self
      end
    end

    DROP_STRING = <<~JS
      var strings = arguments[0],
          el = arguments[1],
          dt = new DataTransfer(),
          opts = { cancelable: true, bubbles: true, dataTransfer: dt };
      for (var i=0; i < strings.length; i++){
        if (dt.items) {
          dt.items.add(strings[i]['data'], strings[i]['type']);
        } else {
          dt.setData(strings[i]['type'], strings[i]['data']);
        }
      }
      var dropEvent = new DragEvent('drop', opts);
      el.dispatchEvent(dropEvent);
    JS

    DROP_FILE = <<~JS
      var el = arguments[0],
          input = arguments[1],
          files = input.files,
          dt = new DataTransfer(),
          opts = { cancelable: true, bubbles: true, dataTransfer: dt };
      input.parentElement.removeChild(input);
      if (dt.items){
        for (var i=0; i<files.length; i++){
          dt.items.add(files[i]);
        }
      } else {
        Object.defineProperty(dt, "files", {
          value: files,
          writable: false
        });
      }
      var dropEvent = new DragEvent('drop', opts);
      el.dispatchEvent(dropEvent);
    JS

    ATTACH_FILE = <<~JS
      (function(){
        var input = document.createElement('INPUT');
        input.type = "file";
        input.id = "_capybara_drop_file";
        input.multiple = true;
        document.body.appendChild(input);
        return input;
      })()
    JS

    MOUSEDOWN_TRACKER = <<~JS
      window.capybara_mousedown_prevented = null;
      document.addEventListener('mousedown', ev => {
        window.capybara_mousedown_prevented = ev.defaultPrevented;
      }, { once: true, passive: true })
    JS

    LEGACY_DRAG_CHECK = <<~JS
      (function(el){
        if ([true, null].indexOf(window.capybara_mousedown_prevented) >= 0){
          return true;
        }

        do {
          if (el.draggable) return false;
        } while (el = el.parentElement );
        return true;
      })(arguments[0])
    JS

    HTML5_DRAG_DROP_SCRIPT = <<~JS
      function rectCenter(rect){
        return new DOMPoint(
          (rect.left + rect.right)/2,
          (rect.top + rect.bottom)/2
        );
      }

      function pointOnRect(pt, rect) {
      	var rectPt = rectCenter(rect);
      	var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);

      	if (pt.x <= rectPt.x) { // left side
      		var minXy = slope * (rect.left - pt.x) + pt.y;
      		if (rect.top <= minXy && minXy <= rect.bottom)
            return new DOMPoint(rect.left, minXy);
      	}

      	if (pt.x >= rectPt.x) { // right side
      		var maxXy = slope * (rect.right - pt.x) + pt.y;
      		if (rect.top <= maxXy && maxXy <= rect.bottom)
            return new DOMPoint(rect.right, maxXy);
      	}

      	if (pt.y <= rectPt.y) { // top side
      		var minYx = (rectPt.top - pt.y) / slope + pt.x;
      		if (rect.left <= minYx && minYx <= rect.right)
            return new DOMPoint(minYx, rect.top);
      	}

      	if (pt.y >= rectPt.y) { // bottom side
      		var maxYx = (rect.bottom - pt.y) / slope + pt.x;
      		if (rect.left <= maxYx && maxYx <= rect.right)
            return new DOMPoint(maxYx, rect.bottom);
      	}

        return new DOMPoint(pt.x,pt.y);
      }

      function dragEnterTarget() {
        target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
        var targetRect = target.getBoundingClientRect();
        var sourceCenter = rectCenter(source.getBoundingClientRect());

        for (var i = 0; i < drop_modifier_keys.length; i++) {
          key = drop_modifier_keys[i];
          if (key == "control"){
            key = "ctrl"
          }
          opts[key + 'Key'] = true;
        }

        var dragEnterEvent = new DragEvent('dragenter', opts);
        target.dispatchEvent(dragEnterEvent);

        // fire 2 dragover events to simulate dragging with a direction
        var entryPoint = pointOnRect(sourceCenter, targetRect)
        var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
        var dragOverEvent = new DragEvent('dragover', dragOverOpts);
        target.dispatchEvent(dragOverEvent);
        window.setTimeout(dragOnTarget, step_delay);
      }

      function dragOnTarget() {
        var targetCenter = rectCenter(target.getBoundingClientRect());
        var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
        var dragOverEvent = new DragEvent('dragover', dragOverOpts);
        target.dispatchEvent(dragOverEvent);
        window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
      }

      function dragLeave(drop, dragOverOpts) {
        var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
        var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
        target.dispatchEvent(dragLeaveEvent);
        if (drop) {
          var dropEvent = new DragEvent('drop', dragLeaveOptions);
          target.dispatchEvent(dropEvent);
        }
        var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
        source.dispatchEvent(dragEndEvent);
        callback.call(true);
      }

      var source = arguments[0],
          target = arguments[1],
          step_delay = arguments[2],
          drop_modifier_keys = arguments[3],
          callback = arguments[4];

      var dt = new DataTransfer();
      var opts = { cancelable: true, bubbles: true, dataTransfer: dt };

      while (source && !source.draggable) {
        source = source.parentElement;
      }

      if (source.tagName == 'A'){
        dt.setData('text/uri-list', source.href);
        dt.setData('text', source.href);
      }
      if (source.tagName == 'IMG'){
        dt.setData('text/uri-list', source.src);
        dt.setData('text', source.src);
      }

      var dragEvent = new DragEvent('dragstart', opts);
      source.dispatchEvent(dragEvent);

      window.setTimeout(dragEnterTarget, step_delay);
    JS
  end
end


def html5_drop(*args)
      if args[0].is_a? String
        input = driver.evaluate_script ATTACH_FILE
        input.set_file(args)
        driver.execute_script DROP_FILE, self, input
      else
        items = args.flat_map do |arg|
          arg.map { |(type, data)| { type: type, data: data } }
        end
        driver.execute_script DROP_STRING, items, self
      end
    end


# frozen_string_literal: true

module Capybara
  module Selenium
    module Scroll
      def scroll_by(x, y)
        driver.execute_script <<~JS, self, x, y
          var el = arguments[0];
          if (el.scrollBy){
            el.scrollBy(arguments[1], arguments[2]);
          } else {
            el.scrollTop = el.scrollTop + arguments[2];
            el.scrollLeft = el.scrollLeft + arguments[1];
          }
        JS
      end

      def scroll_to(element, location, position = nil)
        # location, element = element, nil if element.is_a? Symbol
        if element.is_a? Capybara::Selenium::Node
          scroll_element_to_location(element, location)
        elsif location.is_a? Symbol
          scroll_to_location(location)
        else
          scroll_to_coords(*position)
        end
        self
      end

    private

      def scroll_element_to_location(element, location)
        scroll_opts = case location
        when :top
          'true'
        when :bottom
          'false'
        when :center
          "{behavior: 'instant', block: 'center'}"
        else
          raise ArgumentError, "Invalid scroll_to location: #{location}"
        end
        driver.execute_script <<~JS, element
          arguments[0].scrollIntoView(#{scroll_opts})
        JS
      end

      SCROLL_POSITIONS = {
        top: '0',
        bottom: 'arguments[0].scrollHeight',
        center: '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
      }.freeze

      def scroll_to_location(location)
        driver.execute_script <<~JS, self
          if (arguments[0].scrollTo){
            arguments[0].scrollTo(0, #{SCROLL_POSITIONS[location]});
          } else {
            arguments[0].scrollTop = #{SCROLL_POSITIONS[location]};
          }
        JS
      end

      def scroll_to_coords(x, y)
        driver.execute_script <<~JS, self, x, y
          if (arguments[0].scrollTo){
            arguments[0].scrollTo(arguments[1], arguments[2]);
          } else {
            arguments[0].scrollTop = arguments[2];
            arguments[0].scrollLeft = arguments[1];
          }
        JS
      end
    end
  end
end


def scroll_to(element, location, position = nil)
        # location, element = element, nil if element.is_a? Symbol
        if element.is_a? Capybara::Selenium::Node
          scroll_element_to_location(element, location)
        elsif location.is_a? Symbol
          scroll_to_location(location)
        else
          scroll_to_coords(*position)
        end
        self
      end


def scroll_element_to_location(element, location)
        scroll_opts = case location
        when :top
          'true'
        when :bottom
          'false'
        when :center
          "{behavior: 'instant', block: 'center'}"
        else
          raise ArgumentError, "Invalid scroll_to location: #{location}"
        end
        driver.execute_script <<~JS, element
          arguments[0].scrollIntoView(#{scroll_opts})
        JS
      end


def scroll_to_coords(x, y)
        driver.execute_script <<~JS, self, x, y
          if (arguments[0].scrollTo){
            arguments[0].scrollTo(arguments[1], arguments[2]);
          } else {
            arguments[0].scrollTop = arguments[2];
            arguments[0].scrollLeft = arguments[1];
          }
        JS
      end