lib/appium_lib/driver.rb in appium_lib-6.0.0 vs lib/appium_lib/driver.rb in appium_lib-7.0.0

- old
+ new

@@ -1,8 +1,9 @@ require 'rubygems' require 'ap' require 'selenium-webdriver' +require 'selenium/client/errors' # used in helper.rb for CommandError require 'nokogiri' # common require_relative 'common/helper' require_relative 'common/wait' @@ -64,51 +65,50 @@ # :require is expanded # all keys are converted to symbols # # @param opts [Hash] file: '/path/to/appium.txt', verbose: true # @return [hash] the symbolized hash with updated :app and :require keys - def self.load_appium_txt opts={} - raise 'opts must be a hash' unless opts.kind_of? Hash - raise 'opts must not be empty' if opts.empty? + def self.load_appium_txt(opts = {}) + fail 'opts must be a hash' unless opts.is_a? Hash + fail 'opts must not be empty' if opts.empty? file = opts[:file] - raise 'Must pass file' unless file + fail 'Must pass file' unless file verbose = opts.fetch :verbose, false parent_dir = File.dirname file toml = File.expand_path File.join parent_dir, 'appium.txt' Appium::Logger.info "appium.txt path: #{toml}" if verbose - toml_exists = File.exists? toml + toml_exists = File.exist? toml Appium::Logger.info "Exists? #{toml_exists}" if verbose - raise "toml doesn't exist #{toml}" unless toml_exists + fail "toml doesn't exist #{toml}" unless toml_exists require 'toml' Appium::Logger.info "Loading #{toml}" if verbose data = File.read toml data = TOML::Parser.new(data).parsed # TOML creates string keys. must symbolize - data = Appium::symbolize_keys data + data = Appium.symbolize_keys data Appium::Logger.ap_info data unless data.empty? if verbose if data && data[:caps] && data[:caps][:app] && !data[:caps][:app].empty? data[:caps][:app] = Appium::Driver.absolute_app_path data end # return list of require files as an array # nil if require doesn't exist if data && data[:appium_lib] && data[:appium_lib][:require] r = data[:appium_lib][:require] - r = r.kind_of?(Array) ? r : [r] + r = r.is_a?(Array) ? r : [r] # ensure files are absolute - r.map! do |file| - file = File.exists?(file) ? file : - File.join(parent_dir, file) + r.map! do |f| + file = File.exist?(f) ? f : File.join(parent_dir, f) file = File.expand_path file - File.exists?(file) ? file : nil + File.exist?(file) ? file : nil end r.compact! # remove nils files = [] @@ -117,13 +117,13 @@ unless File.directory? item # save file files << item next # only look inside folders end - Dir.glob(File.expand_path(File.join(item, '**', '*.rb'))) do |file| + Dir.glob(File.expand_path(File.join(item, '**', '*.rb'))) do |f| # do not add folders to the file list - files << File.expand_path(file) unless File.directory? file + files << File.expand_path(f) unless File.directory? f end end # Must not sort files. File order is specified in appium.txt data[:appium_lib][:require] = files @@ -134,49 +134,56 @@ # convert all keys (including nested) to symbols # # based on deep_symbolize_keys & deep_transform_keys from rails # https://github.com/rails/docrails/blob/a3b1105ada3da64acfa3843b164b14b734456a50/activesupport/lib/active_support/core_ext/hash/keys.rb#L84 - def self.symbolize_keys hash - raise 'symbolize_keys requires a hash' unless hash.is_a? Hash + def self.symbolize_keys(hash) + fail 'symbolize_keys requires a hash' unless hash.is_a? Hash result = {} hash.each do |key, value| - key = key.to_sym rescue key + key = key.to_sym rescue key # rubocop:disable Style/RescueModifier result[key] = value.is_a?(Hash) ? symbolize_keys(value) : value end result end + # This method is intended to work with page objects that share + # a common module. For example, Page::HomePage, Page::SignIn + # those could be promoted on with Appium.promote_singleton_appium_methods Page + # + # If you are promoting on an individual class then you should use + # Appium.promote_appium_methods instead. The singleton method is intended + # only for the shared module use case. + # # if modules is a module instead of an array, then the constants of # that module are promoted on. # otherwise, the array of modules will be used as the promotion target. - def self.promote_singleton_appium_methods modules - raise 'Driver is nil' if $driver.nil? + def self.promote_singleton_appium_methods(modules) + fail 'Driver is nil' if $driver.nil? target_modules = [] if modules.is_a? Module modules.constants.each do |sub_module| target_modules << modules.const_get(sub_module) end else - raise 'modules must be a module or an array' unless modules.is_a? Array + fail 'modules must be a module or an array' unless modules.is_a? Array target_modules = modules end target_modules.each do |const| - #noinspection RubyResolve + # noinspection RubyResolve $driver.public_methods(false).each do |m| const.send(:define_singleton_method, m) do |*args, &block| begin super(*args, &block) # promote.rb rescue NoMethodError, ArgumentError $driver.send m, *args, &block if $driver.respond_to?(m) end # override unless there's an existing method with matching arity - end unless const.respond_to?(m) && - const.method(m).arity == $driver.method(m).arity + end unless const.respond_to?(m) && const.method(m).arity == $driver.method(m).arity end end end ## @@ -187,12 +194,24 @@ # To promote methods to all classes: # # ```ruby # Appium.promote_appium_methods Object # ``` - def self.promote_appium_methods class_array - raise 'Driver is nil' if $driver.nil? + # + # It's better to promote on specific classes instead of Object + # + # ```ruby + # # promote on rspec + # Appium.promote_appium_methods RSpec::Core::ExampleGroup + # ``` + # + # ```ruby + # # promote on minispec + # Appium.promote_appium_methods Minitest::Spec + # ``` + def self.promote_appium_methods(class_array) + fail 'Driver is nil' if $driver.nil? # Wrap single class into an array class_array = [class_array] unless class_array.class == Array # Promote Appium driver methods to class instance methods. class_array.each do |klass| $driver.public_methods(false).each do |m| @@ -200,13 +219,14 @@ define_method m do |*args, &block| begin # Prefer existing method. # super will invoke method missing on driver super(*args, &block) - # minitest also defines a name method, - # so rescue argument error - # and call the name method on $driver + + # minitest also defines a name method, + # so rescue argument error + # and call the name method on $driver rescue NoMethodError, ArgumentError $driver.send m, *args, &block if $driver.respond_to?(m) end end end @@ -214,27 +234,27 @@ end nil # return nil end class Driver - @@loaded = false - # 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 # Selenium webdriver capabilities attr_accessor :caps # Custom URL for the selenium server attr_accessor :custom_url # Export session id to textfile in /tmp for 3rd party tools attr_accessor :export_session # Default wait time for elements to appear + # Returns the default client side wait. + # This value is independent of what the server is using + # @return [Integer] attr_accessor :default_wait # Array of previous wait time values attr_accessor :last_waits # Username for use on Sauce Labs attr_accessor :sauce_username @@ -245,10 +265,14 @@ # Device type to request from the appium server attr_accessor :appium_device # Boolean debug mode for the Appium Ruby bindings attr_accessor :appium_debug + # Returns the driver + # @return [Driver] the driver + attr_reader :driver + # Creates a new driver # # ```ruby # require 'rubygems' # require 'appium_lib' @@ -264,25 +288,25 @@ # Appium::Driver.new(opts).start_driver # ``` # # @param opts [Object] A hash containing various options. # @return [Driver] - def initialize opts={} + def initialize(opts = {}) # quit last driver $driver.driver_quit if $driver - raise 'opts must be a hash' unless opts.kind_of? Hash + fail 'opts must be a hash' unless opts.is_a? Hash - opts = Appium::symbolize_keys opts + opts = Appium.symbolize_keys opts # default to {} to prevent nil.fetch and other nil errors @caps = opts[:caps] || {} appium_lib_opts = opts[:appium_lib] || {} # appium_lib specific values @custom_url = appium_lib_opts.fetch :server_url, false @export_session = appium_lib_opts.fetch :export_session, false - @default_wait = appium_lib_opts.fetch :wait, 30 + @default_wait = appium_lib_opts.fetch :wait, 0 @last_waits = [@default_wait] @sauce_username = appium_lib_opts.fetch :sauce_username, ENV['SAUCE_USERNAME'] @sauce_username = nil if !@sauce_username || (@sauce_username.is_a?(String) && @sauce_username.empty?) @sauce_access_key = appium_lib_opts.fetch :sauce_access_key, ENV['SAUCE_ACCESS_KEY'] @sauce_access_key = nil if !@sauce_access_key || (@sauce_access_key.is_a?(String) && @sauce_access_key.empty?) @@ -295,12 +319,12 @@ end # https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile @appium_device = @caps[:platformName] @appium_device = @appium_device.is_a?(Symbol) ? @appium_device : @appium_device.downcase.strip.intern if @appium_device - raise "platformName must be set. Not found in options: #{opts}" unless @appium_device - raise 'platformName must be Android or iOS' unless [:android, :ios].include?(@appium_device) + fail "platformName must be set. Not found in options: #{opts}" unless @appium_device + fail 'platformName must be Android or iOS' unless [:android, :ios].include?(@appium_device) # load common methods extend Appium::Common extend Appium::Device if device_is_android? @@ -323,22 +347,13 @@ Appium::Logger.debug "Debug is: #{@appium_debug}" Appium::Logger.debug "Device is: #{@appium_device}" patch_webdriver_bridge end - # Save global reference to last created Appium driver for top level methods. $driver = self - # Promote exactly once the first time the driver is created. - # Subsequent drivers do not trigger promotion. - unless @@loaded - @@loaded = true - # Promote only on Minitest::Spec (minitest 5) by default - Appium.promote_appium_methods ::Minitest::Spec - end - self # return newly created driver end # Returns a hash of the driver attributes def driver_attributes @@ -349,11 +364,11 @@ last_waits: @last_waits, sauce_username: @sauce_username, sauce_access_key: @sauce_access_key, port: @appium_port, device: @appium_device, - debug: @appium_debug, + debug: @appium_debug } # Return duplicates so attributes are immutable attributes.each do |key, value| attributes[key] = value.duplicable? ? value.dup : value @@ -387,38 +402,38 @@ # 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 'opts must be a hash' unless opts.is_a? Hash + def self.absolute_app_path(opts) + fail 'opts must be a hash' unless opts.is_a? Hash caps = opts[:caps] || {} appium_lib_opts = opts[:appium_lib] || {} server_url = appium_lib_opts.fetch :server_url, false app_path = caps[:app] - raise 'absolute_app_path invoked and app is not set!' if app_path.nil? || app_path.empty? + fail 'absolute_app_path invoked and app is not set!' if app_path.nil? || app_path.empty? # may be absolute path to file on remote server. # if the file is on the remote server then we can't check if it exists return app_path if server_url # 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.match(/^http/) # public URL for Sauce if app_path.match(/^(\/|[a-zA-Z]:)/) # absolute file path app_path = File.expand_path app_path unless File.exist? app_path - raise "App doesn't exist. #{app_path}" unless File.exist? app_path + fail "App doesn't exist. #{app_path}" unless File.exist? app_path return app_path end # if it doesn't contain a slash then it's a bundle id return app_path unless app_path.match(/[\/\\]/) # relative path that must be expanded. # absolute_app_path is called from load_appium_txt # and the txt file path is the base of the app path in that case. app_path = File.expand_path app_path - raise "App doesn't exist #{app_path}" unless File.exist? app_path + fail "App doesn't exist #{app_path}" unless File.exist? app_path app_path end # Get the server url # @return [String] the server url @@ -436,49 +451,46 @@ def restart driver_quit start_driver end - # Returns the driver - # @return [Driver] the driver - def driver - @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 [nil] - def screenshot png_save_path + def screenshot(png_save_path) @driver.save_screenshot png_save_path nil end # Quits the driver # @return [void] def driver_quit # rescue NoSuchDriverError or nil driver + # rubocop:disable Style/RescueModifier @driver.quit rescue nil end # Creates a new global driver and quits the old one if it exists. # # @return [Selenium::WebDriver] the new global driver def start_driver - @client = @client || Selenium::WebDriver::Remote::Http::Default.new - @client.timeout = 999999 + @client ||= Selenium::WebDriver::Remote::Http::Default.new + @client.timeout = 999_999 begin driver_quit @driver = Selenium::WebDriver.for :remote, http_client: @client, desired_capabilities: @caps, url: server_url # Load touch methods. @driver.extend Selenium::WebDriver::DriverExtensions::HasTouchScreen + @driver.extend Selenium::WebDriver::DriverExtensions::HasLocation # export session if @export_session + # rubocop:disable Style/RescueModifier File.open('/tmp/appium_lib_session', 'w') do |f| f.puts @driver.session_id end rescue nil end rescue Errno::ECONNREFUSED @@ -508,11 +520,11 @@ # # ```` # # @param timeout [Integer] the timeout in seconds # @return [void] - def set_wait timeout=nil + def set_wait(timeout = nil) if timeout.nil? # Appium::Logger.info "timeout = @default_wait = @last_wait" # Appium::Logger.info "timeout = @default_wait = #{@last_waits}" timeout = @default_wait = @last_waits.first else @@ -523,17 +535,10 @@ end @driver.manage.timeouts.implicit_wait = timeout end - # Returns the default client side wait. - # This value is independent of what the server is using - # @return [Integer] - def default_wait - @default_wait - end - # Returns existence of element. # # Example: # # exists { button('sign in') } ? puts('true') : puts('false') @@ -542,11 +547,11 @@ # wait to before checking existance # @param post_check [Integer] the amount in seconds to set the # wait to after checking existance # @param search_block [Block] the block to call # @return [Boolean] - def exists pre_check=0, post_check=@default_wait, &search_block + def exists(pre_check = 0, post_check = @default_wait, &search_block) # 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. @@ -566,30 +571,46 @@ # The same as @driver.execute_script # @param script [String] the script to execute # @param args [*args] the args to pass to the script # @return [Object] - def execute_script script, *args + def execute_script(script, *args) @driver.execute_script script, *args end # Calls @driver.find_elements # # @param args [*args] the args to use # @return [Array<Element>] Array is empty when no elements are found. - def find_elements *args - @driver.find_elements *args + def find_elements(*args) + @driver.find_elements(*args) end # Calls @driver.find_elements # # @param args [*args] the args to use # @return [Element] - def find_element *args - @driver.find_element *args + def find_element(*args) + @driver.find_element(*args) 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 = {}) + latitude = opts.fetch(:latitude) + longitude = opts.fetch(:longitude) + altitude = opts.fetch(:altitude, 75) + @driver.set_location(latitude, longitude, altitude) + end + # Quit the driver and Pry. # quit and exit are reserved by Pry. # @return [void] def x driver_quit @@ -599,6 +620,6 @@ end # module Appium # Paging in Pry is annoying :q required to exit. # With pager disabled, the output is similar to IRB # Only set if Pry is defined. -Pry.config.pager = false if defined?(Pry) \ No newline at end of file +Pry.config.pager = false if defined?(Pry)