lib/appium_lib/driver.rb



# frozen_string_literal: true

# Licensed 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.

# Load only Minitest is loaded
if defined?(Minitest::VERSION)
  # Fix uninitialized constant Minitest (NameError)
  module Minitest
    # Fix superclass mismatch for class Spec
    class Runnable
    end

    begin
      class Test < Runnable
      end
    rescue TypeError => te
      # http://docs.seattlerb.org/minitest/History_rdoc.html#label-5.11.0+-2F+2018-01-01
      # for 5.11.0/5.11.1
      # `Minitest::Test` became a subclass of `Minitest::Result`
      raise TypeError, te.message unless te.message == 'superclass mismatch for class Test'

      class Test < Result
      end
    end
  end
end

require 'appium_lib_core'
require 'uri'

module Appium
  class Driver
    # @private
    class << self
      def convert_to_symbol(value)
        if value.nil?
          value
        else
          value.to_sym
        end
      end

      # @private
      def get_cap(caps, name)
        name_with_prefix = "#{::Appium::Core::Base::Bridge::APPIUM_PREFIX}#{name}"
        caps[convert_to_symbol name] ||
          caps[name] ||
          caps[convert_to_symbol name_with_prefix] ||
          caps[name_with_prefix]
      end
    end

    # attr readers are promoted to global scope. To avoid clobbering, they're
    # made available via the driver_attributes method
    #
    # attr_accessor is repeated for each one so YARD documents them properly.

    # The amount to sleep in seconds before every webdriver http call.
    attr_accessor :global_webdriver_http_sleep

    # SauceLab's settings
    attr_reader :sauce
    # Username for use on Sauce Labs. Set `false` to disable Sauce, even when SAUCE_USERNAME is in ENV.
    # same as @sauce.username
    attr_reader :sauce_username
    # Access Key for use on Sauce Labs. Set `false` to disable Sauce, even when SAUCE_ACCESS_KEY is in ENV.
    # same as @sauce.access_key
    attr_reader :sauce_access_key
    # Override the Sauce Appium endpoint to allow e.g. TestObject tests
    # same as @sauce.endpoint
    attr_reader :sauce_endpoint

    # from Core
    # read http://www.rubydoc.info/github/appium/ruby_lib_core/Appium/Core/Driver
    attr_reader :caps
    attr_reader :custom_url
    attr_reader :default_wait
    attr_reader :appium_port
    attr_reader :appium_device
    attr_reader :automation_name
    attr_reader :listener
    attr_reader :http_client
    attr_reader :appium_wait_timeout
    attr_reader :appium_wait_interval

    # Appium's server version
    attr_reader :appium_server_status
    # Boolean debug mode for the Appium Ruby bindings
    attr_reader :appium_debug
    # Returns the driver
    # @return [Driver] the driver
    attr_reader :driver
    # Instance of Appium::Core::Driver
    attr_reader :core

    # Creates a new driver. The driver is defined as global scope by default.
    # We can avoid defining global driver.
    #
    # @example
    #
    #   require 'rubygems'
    #   require 'appium_lib'
    #
    #   # platformName takes a string or a symbol.
    #   # Start iOS driver with global scope
    #   opts = {
    #            caps: {
    #              platformName: :ios,
    #              app: '/path/to/MyiOS.app'
    #            },
    #            appium_lib: {
    #              server_url: 'http://127.0.0.1:4723'
    #              wait_timeout: 30
    #            }
    #          }
    #   appium_driver = Appium::Driver.new(opts, true)
    #   appium_driver.start_driver
    #
    #   # Start Android driver with global scope
    #   opts = {
    #            caps: {
    #              platformName: :android,
    #              app: '/path/to/my.apk'
    #            },
    #            appium_lib: {
    #              wait_timeout: 30,
    #              wait_interval: 1
    #            }
    #          }
    #   appium_driver = Appium::Driver.new(opts, true)
    #   appium_driver.start_driver
    #
    #   # Start iOS driver without global scope
    #   opts = {
    #            caps: {
    #              platformName: :ios,
    #              app: '/path/to/MyiOS.app'
    #            },
    #            appium_lib: {
    #              wait_timeout: 30
    #            }
    #          }
    #   appium_driver = Appium::Driver.new(opts, false)
    #   appium_driver.start_driver
    #
    #   # Start iOS driver without global scope
    #   opts = {
    #            caps: {
    #              platformName: :ios,
    #              app: '/path/to/MyiOS.app'
    #            },
    #            appium_lib: {
    #              wait_timeout: 30
    #            },
    #            global_driver: false
    #          }
    #   appium_driver = Appium::Driver.new(opts)
    #   appium_driver.start_driver
    #
    # @param opts [Object] A hash containing various options.
    # @param global_driver [Bool] A bool require global driver before initialize.
    # @return [Driver]
    def initialize(opts = {}, global_driver = false)
      # Capybara can't put `global_driver` as the 2nd argument.
      global_driver = opts.delete :global_driver if global_driver.nil?

      $driver&.driver_quit if global_driver

      raise ArgumentError, 'opts must be a hash' unless opts.is_a? Hash

      @core = ::Appium::Core.for(opts)
      extend ::Appium::Core::Device

      opts = Appium.symbolize_keys opts
      appium_lib_opts = opts[:appium_lib] || {}

      @caps = @core.caps
      @custom_url = @core.custom_url
      @default_wait = @core.default_wait || 0
      @appium_port = @core.port
      @appium_wait_timeout = @core.wait_timeout
      @appium_wait_interval = @core.wait_interval
      @listener = @core.listener
      @appium_device = @core.device
      @automation_name = @core.automation_name

      # Arrange the app capability. This must be after @core = ::Appium::Core.for(opts)
      set_app_path(opts)

      # enable debug patch
      @appium_debug = appium_lib_opts.fetch :debug, !!defined?(Pry) # rubocop:disable Style/DoubleNegation
      set_sauce_related_values(appium_lib_opts)

      # Extend Common methods
      extend Appium::Common
      extend Appium::Device

      # Extend each driver's methods
      extend_for(device: @core.device, automation_name: @core.automation_name)

      # for command

      if @appium_debug
        Appium::Logger.debug opts unless opts.empty?
        Appium::Logger.debug "Debug is: #{@appium_debug}"
        Appium::Logger.debug "Device is: #{@core.device}"
      end

      # Save global reference to last created Appium driver for top level methods.
      $driver = self if global_driver

      self # rubocop:disable Lint/Void # return newly created driver
    end

    private

    # @private
    def extend_for(device:, automation_name:)
      case device
      when :android
        case automation_name
        when :uiautomator2
          ::Appium::Android::Uiautomator2::Bridge.for(self)
        when :espresso
          ::Appium::Android::Espresso::Bridge.for(self)
        else # default and UiAutomator
          ::Appium::Android::Bridge.for(self)
        end
      when :ios, :tvos
        # default and XCUITest
        ::Appium::Ios::Xcuitest::Bridge.for(self)
      when :mac
        # no Mac specific extentions
        Appium::Logger.debug('mac')
      when :windows
        # no windows specific extentions
        Appium::Logger.debug('windows')
      when :tizen
        # https://github.com/Samsung/appium-tizen-driver
        Appium::Logger.debug('tizen')
      when :youiengine
        # https://github.com/YOU-i-Labs/appium-youiengine-driver
        Appium::Logger.debug('YouiEngine')
      else
        case automation_name
        when :youiengine
          # https://github.com/YOU-i-Labs/appium-youiengine-driver
          Appium::Logger.debug('YouiEngine')
        else
          Appium::Logger.debug('no device matched') # core also shows warning message
        end
      end
    end

    # @private
    # Deprecated. TODO: remove
    def set_app_path(opts)
      return unless @core.caps

      # return the path exists on the local
      app_path = Driver.get_cap(@core.caps, 'app')
      return if app_path.nil?
      return if File.exist?(app_path)

      @core.caps['app'] = self.class.absolute_app_path opts
    end

    # @private
    def set_sauce_related_values(appium_lib_opts)
      @sauce = Appium::SauceLabs.new(appium_lib_opts)
      @sauce_username   = @sauce.username
      @sauce_access_key = @sauce.access_key
      @sauce_endpoint   = @sauce.endpoint
    end

    public

    # Returns a hash of the driver attributes
    def driver_attributes
      {
        caps:                @core.caps,
        automation_name:     @core.automation_name,
        custom_url:          @core.custom_url,
        default_wait:        @default_wait,
        sauce_username:      @sauce.username,
        sauce_access_key:    @sauce.access_key,
        sauce_endpoint:      @sauce.endpoint,
        port:                @core.port,
        device:              @core.device,
        debug:               @appium_debug,
        listener:            @listener,
        wait_timeout:        @core.wait_timeout,
        wait_interval:       @core.wait_interval
      }
    end

    def device_is_android?
      @core.device == :android
    end

    def device_is_ios?
      @core.device == :ios
    end

    def device_is_windows?
      @core.device == :windows
    end

    # Return true if automationName is 'uiautomator2'
    # @return [Boolean]
    def automation_name_is_uiautomator2?
      !@core.automation_name.nil? && @core.automation_name == :uiautomator2
    end

    # Return true if automationName is 'Espresso'
    # @return [Boolean]
    def automation_name_is_espresso?
      !@core.automation_name.nil? && @core.automation_name == :espresso
    end

    # Return true if automationName is 'XCUITest'
    # @return [Boolean]
    def automation_name_is_xcuitest?
      !@core.automation_name.nil? && @core.automation_name == :xcuitest
    end

    # An entry point to chain W3C actions
    # Read https://www.rubydoc.info/github/appium/ruby_lib_core/Appium/Core/Base/Bridge/W3C#action-instance_method
    #
    # @return [Selenium::WebDriver::PointerActions]
    #
    # @example
    #
    #     element = find_element(:id, "some id")
    #     action.click(element).perform # The `click` is a part of `PointerActions`
    #
    def action
      @driver&.action
    end

    # Returns the server's version info
    #
    # @example
    #   {
    #     "build" => {
    #         "version" => "0.18.1",
    #         "revision" => "d242ebcfd92046a974347ccc3a28f0e898595198"
    #     }
    #   }
    #
    # @return [Hash]
    def appium_server_version
      @core.appium_server_version
    rescue Selenium::WebDriver::Error::WebDriverError => ex
      raise ::Appium::Core::Error::ServerError unless ex.message.include?('content-type=""')

      # server (TestObject for instance) does not respond to status call
      {}
    end
    alias remote_status appium_server_version

    # Return the platform version as an array of integers
    # @return [Array<Integer>]
    def platform_version
      return [] if @driver.nil?

      p_version = @driver.capabilities['platformVersion']
      p_version.split('.').map(&:to_i)
    end

    # Returns the client's version info
    #
    # @example
    #
    #   {
    #       "version" => "9.1.1"
    #   }
    #
    # @return [Hash]
    def appium_client_version
      { version: ::Appium::VERSION }
    end

    # [Deprecated] Converts app_path to an absolute path.
    #
    # opts is the full options hash (caps and appium_lib). If server_url is set
    # then the app path is used as is.
    #
    # if app isn't set then an error is raised.
    #
    # @return [String] APP_PATH as an absolute path
    def self.absolute_app_path(opts)
      raise ArgumentError, 'opts must be a hash' unless opts.is_a? Hash

      caps = opts[:caps] || opts['caps'] || {}
      app_path = get_cap(caps, 'app')
      raise ArgumentError, 'absolute_app_path invoked and app is not set!' if app_path.nil? || app_path.empty?
      # Sauce storage API. http://saucelabs.com/docs/rest#storage
      return app_path if app_path.start_with? 'sauce-storage:'
      return app_path if app_path =~ URI::DEFAULT_PARSER.make_regexp # public URL for Sauce

      ::Appium::Logger.warn('[Deprecation] Converting the path to absolute path will be removed. ' \
                            'Please specify the full path which can be accessible from the appium server')

      absolute_app_path = File.expand_path app_path
      if File.exist? absolute_app_path
        absolute_app_path
      else
        ::Appium::Logger.info("Use #{app_path}")
        app_path
      end
    end

    # Get the server url
    # @return [String] the server url
    def server_url
      return @core.custom_url if @core.custom_url
      return @sauce.server_url if @sauce.sauce_server_url?

      "http://127.0.0.1:#{@core.port}"
    end

    # Restarts the driver
    # @return [Driver] the driver
    def restart
      driver_quit
      start_driver
    end

    # Takes a png screenshot and saves to the target path.
    #
    # @example
    #
    #   screenshot '/tmp/hi.png'
    #
    # @param png_save_path [String] the full path to save the png
    # @return [File]
    def screenshot(png_save_path)
      @driver&.save_screenshot png_save_path
    end

    # Takes a png screenshot of particular element's area
    #
    # @example
    #
    #   el = find_element :accessibility_id, zzz
    #   element_screenshot el, '/tmp/hi.png'
    #
    # @param [String] element Element take a screenshot
    # @param [String] png_save_path the full path to save the png
    # @return [File]
    def element_screenshot(element, png_save_path)
      @driver&.take_element_screenshot element, png_save_path
      nil
    end

    # Quits the driver
    # @return [void]
    def driver_quit
      @driver&.quit
      @driver = nil
    rescue Selenium::WebDriver::Error::WebDriverError
      nil
    end
    alias quit_driver driver_quit

    # Get the device window's size.
    # @return [Selenium::WebDriver::Dimension]
    #
    # @example
    #
    #   size = @driver.window_size
    #   size.width #=> Integer
    #   size.height #=> Integer
    #
    def window_size
      # maybe exception is expected as no driver created
      raise NoDriverInstanceError if @driver.nil?

      @driver.window_size
    end

    # Get the device window's rect.
    # @return [Selenium::WebDriver::Rectangle]
    #
    # @example
    #
    #   size = @driver.window_size
    #   size.width #=> Integer
    #   size.height #=> Integer
    #   size.x #=> Integer
    #   size.y #=> Integer
    #
    def window_rect
      raise NoDriverInstanceError if @driver.nil?

      @driver.window_rect
    end

    # Creates a new global driver and quits the old one if it exists.
    # You can customise http_client as the following
    #
    # Read http://www.rubydoc.info/github/appium/ruby_lib_core/Appium/Core/Device to understand more what the driver
    # can call instance methods.
    #
    # @example
    #
    #   require 'rubygems'
    #   require 'appium_lib'
    #
    #   # platformName takes a string or a symbol.
    #   # Start iOS driver
    #   opts = {
    #            caps: {
    #              platformName: :ios,
    #              app: '/path/to/MyiOS.app'
    #            },
    #            appium_lib: {
    #              wait_timeout: 30
    #            }
    #          }
    #   appium_driver = Appium::Driver.new(opts) #=> return an Appium::Driver instance
    #   appium_driver.start_driver #=> return an Appium::Core::Base::Driver
    #
    # @option http_client_ops [Hash] :http_client Custom HTTP Client
    # @option http_client_ops [Hash] :open_timeout Custom open timeout for http client.
    # @option http_client_ops [Hash] :read_timeout Custom read timeout for http client.
    # @return [Selenium::WebDriver] the new global driver
    def start_driver(http_client_ops = { http_client: nil, open_timeout: 999_999, read_timeout: 999_999 })
      if http_client_ops[:http_client].nil?
        http_client = ::Appium::Http::Default.new(open_timeout: http_client_ops[:open_timeout],
                                                  read_timeout: http_client_ops[:read_timeout])
      end

      # TODO: do not kill the previous session in the future version.
      if $driver.nil?
        driver_quit
      else
        $driver.driver_quit
      end

      # If automationName is set only in server side, then the following automation_name should be nil before
      # starting driver.
      automation_name = @core.automation_name

      @driver = @core.start_driver(server_url: server_url,
                                   http_client_ops: {
                                     http_client: http_client,
                                     open_timeout: 999_999,
                                     read_timeout: 999_999
                                   })
      @http_client = @core.http_client

      # if automation_name was nil before start_driver, then re-extend driver specific methods
      # to be able to extend correctly.
      extend_for(device: @core.device, automation_name: @core.automation_name) if automation_name.nil?

      @appium_server_status = appium_server_version

      @driver
    end

    # To ignore error for Espresso Driver
    def set_implicit_wait(wait)
      @driver.manage.timeouts.implicit_wait = wait
    rescue Selenium::WebDriver::Error::UnknownError => e
      unless e.message.include?('The operation requested is not yet implemented by Espresso driver')
        raise ::Appium::Core::Error::ServerError
      end

      {}
    end

    # Set implicit wait to zero.
    def no_wait
      @driver&.manage&.timeouts&.implicit_wait = 0
    end

    # Set implicit wait. Default to @default_wait.
    #
    # @example
    #
    #   set_wait 2
    #   set_wait # @default_wait
    #
    #
    # @param timeout [Integer] the timeout in seconds
    # @return [void]
    def set_wait(timeout = nil)
      timeout = @default_wait if timeout.nil?
      @driver&.manage&.timeouts&.implicit_wait = timeout
    end

    # Returns existence of element.
    #
    # Example:
    #
    # exists { button('sign in') } ? puts('true') : puts('false')
    #
    # @param [Integer] pre_check The amount in seconds to set the
    #                             wait to before checking existence
    # @param [Integer] post_check The amount in seconds to set the
    #                             wait to after checking existence
    # @yield The block to call
    # @return [Boolean]
    def exists(pre_check = 0, post_check = @default_wait)
      # do not uset set_wait here.
      # it will cause problems with other methods reading the default_wait of 0
      # which then gets converted to a 1 second wait.
      @driver&.manage&.timeouts&.implicit_wait = pre_check
      # the element exists unless an error is raised.
      exists = true

      begin
        yield # search for element
      rescue StandardError
        exists = false # error means it's not there
      end

      # restore wait
      @driver&.manage&.timeouts&.implicit_wait = post_check if post_check != pre_check

      exists
    end

    # The same as @driver.execute_script
    # @param [String] script The script to execute
    # @param [*args] args The args to pass to the script
    # @return [Object]
    def execute_script(script, *args)
      raise NoDriverInstanceError if @driver.nil?

      @driver.execute_script script, *args
    end

    ###
    # Wrap calling selenium webdrier APIs via ruby_core
    ###
    # Get the window handles of open browser windows
    def execute_async_script(script, *args)
      raise NoDriverInstanceError if @driver.nil?

      @driver.execute_async_script script, *args
    end

    # Run a set of script against the current session, allowing execution of many commands in one Appium request.
    # Supports {https://webdriver.io/docs/api.html WebdriverIO} API so far.
    # Please read {http://appium.io/docs/en/commands/session/execute-driver command API} for more details
    # about acceptable scripts and the output.
    #
    # @param [String] script The string consisting of the script itself
    # @param [String] type The name of the script type.
    #                      Defaults to 'webdriverio'. Depends on server implementation which type is supported.
    # @param [Integer] timeout_ms The number of `ms` Appium should wait for the script to finish
    #                          before killing it due to timeout.
    #
    # @return [Appium::Core::Base::Device::ExecuteDriver::Result] The script result parsed by
    #                          Appium::Core::Base::Device::ExecuteDriver::Result.
    #
    # @raise [::Selenium::WebDriver::Error::UnknownError] If something error happens in the script.
    #                                                     It has the original message.
    #
    # @example
    #      script = <<~SCRIPT
    #        const status = await driver.status();
    #        console.warn('warning message');
    #        return [status];
    #      SCRIPT
    #      r = @@driver.execute_driver(script: script, type: 'webdriverio', timeout: 10_000)
    #      r        #=> An instance of Appium::Core::Base::Device::ExecuteDriver::Result
    #      r.result #=> The `result` key part as the result of the script
    #      r.logs   #=> The `logs` key part as `{'log' => [], 'warn' => [], 'error' => []}`
    #
    def execute_driver(script: '', type: 'webdriverio', timeout_ms: nil)
      raise NoDriverInstanceError if @driver.nil?

      @driver.execute_driver(script: script, type: type, timeout_ms: timeout_ms)
    end

    def window_handles
      raise NoDriverInstanceError if @driver.nil?

      @driver.window_handles
    end

    # Get the current window handle
    def window_handle
      raise NoDriverInstanceError if @driver.nil?

      @driver.window_handle
    end

    def navigate
      raise NoDriverInstanceError if @driver.nil?

      @driver.navigate
    end

    def manage
      raise NoDriverInstanceError if @driver.nil?

      @driver.manage
    end

    def get(url)
      raise NoDriverInstanceError if @driver.nil?

      @driver.get(url)
    end

    def current_url
      raise NoDriverInstanceError if @driver.nil?

      @driver.current_url
    end

    def title
      raise NoDriverInstanceError if @driver.nil?

      @driver.title
    end

    # @return [TargetLocator]
    # @see TargetLocator
    def switch_to
      raise NoDriverInstanceError if @driver.nil?

      @driver.switch_to
    end
    ###
    # End core
    ###

    # Calls @driver.find_elements_with_appium
    #
    # @example
    #
    #   @driver = Appium::Driver.new(opts, false)
    #   @driver.start_driver
    #   @driver.find_elements :predicate, yyy
    #
    # If you call `Appium.promote_appium_methods`, you can call `find_elements` directly.
    #
    # @example
    #
    #   @driver = Appium::Driver.new(opts, false)
    #   @driver.start_driver
    #   @driver.find_elements :predicate, yyy
    #
    # If you call `Appium.promote_appium_methods`, you can call `find_elements` directly.
    #
    # @param [*args] args The args to use
    # @return [Array<Element>] Array is empty when no elements are found.
    def find_elements(*args)
      raise NoDriverInstanceError if @driver.nil?

      @driver.find_elements(*args)
    end

    # Calls @driver.find_element
    #
    # @example
    #
    #   @driver = Appium::Driver.new(opts, false)
    #   @driver.start_driver
    #   @driver.find_element :accessibility_id, zzz
    #
    # If you call `Appium.promote_appium_methods`, you can call `find_element` directly.
    #
    # @param [*args] args The args to use
    # @return [Element]
    def find_element(*args)
      raise NoDriverInstanceError if @driver.nil?

      @driver.find_element(*args)
    end

    # Return ImageElement if current view has a partial image
    #
    # @param [String] png_img_path A path to a partial image you'd like to find
    #
    # @return [::Appium::Core::ImageElement]
    # @raise [::Appium::Core::Error::NoSuchElementError|::Appium::Core::Error::CoreError] No such element
    #
    # @example
    #
    #     @driver.find_element_by_image './test/functional/data/test_element_image.png'
    #
    def find_element_by_image(png_img_path)
      raise NoDriverInstanceError if @driver.nil?

      @driver.find_element_by_image(png_img_path)
    end

    # Return ImageElement if current view has partial images
    #
    # @param [[String]] png_img_paths Paths to a partial image you'd like to find
    #
    # @return [[::Appium::Core::ImageElement]]
    # @return [::Appium::Core::Error::CoreError]
    #
    # @example
    #
    #     @driver.find_elements_by_image ['./test/functional/data/test_element_image.png']
    #
    def find_elements_by_image(png_img_paths)
      raise NoDriverInstanceError if @driver.nil?

      @driver.find_elements_by_image(png_img_paths)
    end

    # Calls @driver.set_location
    #
    # @note This method does not work on real devices.
    #
    # @param  [Hash] opts consisting of:
    # @option opts [Float] :latitude the latitude in degrees (required)
    # @option opts [Float] :longitude the longitude in degees (required)
    # @option opts [Float] :altitude the altitude, defaulting to 75
    # @return [Selenium::WebDriver::Location] the location constructed by the selenium webdriver
    def set_location(opts = {})
      raise NoDriverInstanceError if @driver.nil?

      latitude = opts.fetch(:latitude)
      longitude = opts.fetch(:longitude)
      altitude = opts.fetch(:altitude, 75)
      @driver.set_location(latitude, longitude, altitude)
    end

    # @since Appium 1.16.0
    #
    # Logs a custom event. The event is available via {::Appium::Core::Events#get}.
    #
    # @param [String] vendor The vendor prefix for the event
    # @param [String] event The name of event
    # @return [nil]
    #
    # @example
    #
    #   log_event vendor: 'appium', event: 'funEvent'
    #
    #   log_event = { vendor: 'appium', event: 'anotherEvent' }
    #   log_events #=> {...., 'appium:funEvent' => [1572957315, 1572960305],
    #              #          'appium:anotherEvent' => 1572959315}
    #
    def log_event(vendor:, event:)
      raise NoDriverInstanceError if @driver.nil?

      @driver.logs.event vendor: vendor, event: event
    end

    def log_event=(log_event)
      raise if @driver.nil?
      unless log_event.is_a?(Hash)
        raise ::Appium::Core::Error::ArgumentError('log_event should be Hash like { vendor: "appium", event: "funEvent"}')
      end

      @driver.logs.event vendor: log_event[:vendor], event: log_event[:event]
    end

    # @since Appium 1.16.0
    # Returns events with filtering with 'type'. Defaults to all available events.
    #
    # @param [String] type The type of events to get
    # @return [Hash]
    #
    # @example
    #
    #   log_events #=> {}
    #   log_events #=> {'commands' => [{'cmd' => 123455, ....}], 'startTime' => 1572954894127, }
    #
    def log_events(type = nil)
      raise NoDriverInstanceError if @driver.nil?

      @driver.logs.events(type)
    end

    # Quit the driver and Pry.
    # quit and exit are reserved by Pry.
    # @return [void]
    def x
      driver_quit
      exit # exit pry
    end
  end # class Driver
end # module Appium