lib/selenium/webdriver/remote/capabilities.rb



# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

module Selenium
  module WebDriver
    module Remote
      #
      # Specification of the desired and/or actual capabilities of the browser that the
      # server is being asked to create.
      #

      class Capabilities
        KNOWN = [
          :browser_name,
          :browser_version,
          :platform_name,
          :accept_insecure_certs,
          :page_load_strategy,
          :proxy,
          :set_window_rect,
          :timeouts,
          :unhandled_prompt_behavior,
          :strict_file_interactability,
          :web_socket_url,

          # remote-specific (webdriver.remote.sessionid)
          :remote_session_id
        ].freeze

        (KNOWN - %i[proxy timeouts]).each do |key|
          define_method key do
            @capabilities[key]
          end

          define_method :"#{key}=" do |value|
            @capabilities[key] = value
          end
        end

        #
        # Convenience methods for the common choices.
        #

        class << self
          def always_match(capabilities)
            new(always_match: capabilities)
          end

          def first_match(*capabilities)
            new(first_match: capabilities)
          end

          #
          # @api private
          #

          def json_create(data)
            data = data.dup
            caps = new

            process_timeouts(caps, data.delete('timeouts'))

            if data.key?('proxy')
              proxy = data.delete('proxy')
              caps.proxy = Proxy.json_create(proxy) unless proxy.nil? || proxy.empty?
            end

            # Remote Server Specific
            if data.key?('webdriver.remote.sessionid')
              caps[:remote_session_id] =
                data.delete('webdriver.remote.sessionid')
            end

            KNOWN.each do |cap|
              data_value = camel_case(cap)
              caps[cap] = data.delete(data_value) if data.key?(data_value)
            end

            # any remaining pairs will be added as is, with no conversion
            caps.merge!(data)

            caps
          end

          def camel_case(str_or_sym)
            str_or_sym.to_s.gsub(/_([a-z])/) { Regexp.last_match(1)&.upcase }
          end

          private

          def process_timeouts(caps, timeouts)
            return if timeouts.nil?

            caps.implicit_timeout = timeouts['implicit']
            caps.page_load_timeout = timeouts['pageLoad']
            caps.script_timeout = timeouts['script']
          end
        end

        #
        # @param [Hash] opts
        # @option :browser_name             [String] required browser name
        # @option :browser_version          [String] required browser version number
        # @option :platform_name            [Symbol] one of :any, :win, :mac, or :x
        # @option :accept_insecure_certs    [Boolean] does the driver accept insecure SSL certifications?
        # @option :proxy                    [Selenium::WebDriver::Proxy, Hash] proxy configuration
        #
        # @api public
        #

        def initialize(opts = {})
          @capabilities = {}
          self.proxy = opts.delete(:proxy) if opts[:proxy]
          @capabilities.merge!(opts)
        end

        #
        # Allows setting arbitrary capabilities.
        #

        def []=(key, value)
          @capabilities[key] = value
        end

        def [](key)
          @capabilities[key]
        end

        def merge!(other)
          if other.respond_to?(:capabilities, true) && other.capabilities.is_a?(Hash)
            @capabilities.merge! other.capabilities
          elsif other.is_a? Hash
            @capabilities.merge! other
          else
            raise ArgumentError, 'argument should be a Hash or implement #capabilities'
          end
        end

        def proxy
          @capabilities[:proxy]
        end

        def proxy=(proxy)
          case proxy
          when Hash
            @capabilities[:proxy] = Proxy.new(proxy)
          when Proxy, nil
            @capabilities[:proxy] = proxy
          else
            raise TypeError, "expected Hash or #{Proxy.name}, got #{proxy.inspect}:#{proxy.class}"
          end
        end

        def timeouts
          @capabilities[:timeouts] ||= {}
        end

        def timeouts=(timeouts)
          @capabilities[:timeouts] = timeouts
        end

        def implicit_timeout
          timeouts[:implicit]
        end

        def implicit_timeout=(timeout)
          timeouts[:implicit] = timeout
        end

        def page_load_timeout
          timeouts[:page_load] || timeouts[:pageLoad]
        end

        def page_load_timeout=(timeout)
          timeouts[:page_load] = timeout
        end

        def script_timeout
          timeouts[:script]
        end

        def script_timeout=(timeout)
          timeouts[:script] = timeout
        end

        #
        # @api private
        #

        def as_json(*)
          @capabilities.each_with_object({}) do |(key, value), hash|
            hash[convert_key(key)] = process_capabilities(key, value, hash)
          end
        end

        def to_json(*)
          JSON.generate as_json
        end

        def ==(other)
          return false unless other.is_a? self.class

          as_json == other.as_json
        end

        alias eql? ==

        protected

        attr_reader :capabilities

        private

        def process_capabilities(key, value, hash)
          case value
          when Array
            value.map { |v| process_capabilities(key, v, hash) }
          when Hash
            value.each_with_object({}) do |(k, v), h|
              h[convert_key(k)] = process_capabilities(k, v, h)
            end
          when Capabilities, Options
            value.as_json
          else
            convert_value(key, value)
          end
        end

        def convert_key(key)
          case key
          when String
            key.to_s
          when Symbol
            self.class.camel_case(key)
          else
            raise TypeError, "expected String or Symbol, got #{key.inspect}:#{key.class}"
          end
        end

        def convert_value(key, value)
          case key
          when :platform
            value.to_s.upcase
          when :proxy
            value&.as_json
          when :unhandled_prompt_behavior
            value.is_a?(Symbol) ? value.to_s.tr('_', ' ') : value
          else
            value
          end
        end
      end # Capabilities
    end # Remote
  end # WebDriver
end # Selenium