lib/appium_lib/driver.rb in appium_lib-0.24.1 vs lib/appium_lib/driver.rb in appium_lib-1.0.0
- old
+ new
@@ -1,149 +1,155 @@
-# encoding: utf-8
-=begin
-Based on simple_test.rb
-https://github.com/appium/appium/blob/82995f47408530c80c3376f4e07a1f649d96ba22/sample-code/examples/ruby/simple_test.rb
-https://github.com/appium/appium/blob/c58eeb66f2d6fa3b9a89d188a2e657cca7cb300f/LICENSE
-=end
-
require 'rubygems'
require 'ap'
+require 'selenium-webdriver'
+require 'nokogiri'
-# Support OpenStruct in Awesome Print
-# /awesome_print/lib/awesome_print/formatter.rb
-# upstream issue: https://github.com/michaeldv/awesome_print/pull/36
-class AwesomePrint::Formatter
- remove_const :CORE if defined?(CORE)
- CORE = [ :array, :hash, :class, :file, :dir, :bigdecimal, :rational, :struct, :openstruct, :method, :unboundmethod ]
+# patch ap
+require_relative 'awesome_print/ostruct'
- def awesome_openstruct target
- awesome_hash target.marshal_dump
+# common
+require_relative 'common/helper'
+require_relative 'common/patch'
+require_relative 'common/version'
+require_relative 'common/element/window'
+
+# ios
+require_relative 'ios/helper'
+require_relative 'ios/patch'
+
+require_relative 'ios/element/alert'
+require_relative 'ios/element/button'
+require_relative 'ios/element/generic'
+require_relative 'ios/element/textfield'
+require_relative 'ios/element/text'
+require_relative 'ios/mobile_methods'
+
+# android
+require_relative 'android/dynamic'
+require_relative 'android/helper'
+require_relative 'android/patch'
+require_relative 'android/element/alert'
+require_relative 'android/element/button'
+require_relative 'android/element/generic'
+require_relative 'android/element/textfield'
+require_relative 'android/element/text'
+require_relative 'android/mobile_methods'
+
+# device methods
+require_relative 'device/device'
+require_relative 'device/touch_actions'
+require_relative 'device/multi_touch'
+
+# Fix uninitialized constant Minitest (NameError)
+module Minitest
+ # Fix superclass mismatch for class Spec
+ class Runnable
end
+ class Test < Runnable
+ end
+ class Spec < Test
+ end
end
-# Load appium.txt (toml format) into system ENV
-# the basedir of this file + appium.txt is what's used
-# @param opts [Hash] file: '/path/to/appium.txt', verbose: true
-# @return [Array<String>] the require files. nil if require doesn't exist
-def load_appium_txt opts
- raise 'opts must be a hash' unless opts.kind_of? Hash
- opts.each_pair { |k,v| opts[k.to_s.downcase.strip.intern] = v }
- opts = {} if opts.nil?
- file = opts.fetch :file, nil
- raise 'Must pass file' unless file
- verbose = opts.fetch :verbose, false
- # Check for env vars in .txt
- parent_dir = File.dirname file
- toml = File.expand_path File.join parent_dir, 'appium.txt'
- puts "appium.txt path: #{toml}" if verbose
- # @private
- def update data, *args
- args.each do |name|
- var = data[name]
- ENV[name] = var if var
- end
- end
+module Appium
+ # Load appium.txt (toml format)
+ # the basedir of this file + appium.txt is what's used
+ #
+ # ```
+ # [caps]
+ # app = "path/to/app"
+ #
+ # [appium_lib]
+ # port = 8080
+ # ```
+ #
+ # :app is expanded
+ # :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?
- toml_exists = File.exists? toml
- puts "Exists? #{toml_exists}" if verbose
- data = nil
+ file = opts[:file]
+ raise 'Must pass file' unless file
+ verbose = opts.fetch :verbose, false
- if toml_exists
+ parent_dir = File.dirname file
+ toml = File.expand_path File.join parent_dir, 'appium.txt'
+ puts "appium.txt path: #{toml}" if verbose
+
+ toml_exists = File.exists? toml
+ puts "Exists? #{toml_exists}" if verbose
+
+ raise "toml doesn't exist #{toml}" unless toml_exists
require 'toml'
puts "Loading #{toml}" if verbose
- # bash requires A="OK"
- # toml requires A = "OK"
- #
- # A="OK" => A = "OK"
data = File.read toml
-
- data = data.split("\n").map do |line|
- line.sub /([^\s])\=/, "\\1 = "
- end.join "\n"
-
data = TOML::Parser.new(data).parsed
+ # TOML creates string keys. must symbolize
+ data = Appium::symbolize_keys data
ap data unless data.empty? if verbose
- update data, 'APP_PATH', 'APP_APK', 'APP_PACKAGE',
- 'APP_ACTIVITY', 'APP_WAIT_ACTIVITY',
- 'DEVICE'
+ if data && data[:caps] && data[:caps][:app]
+ data[:caps][:app] = Appium::Driver.absolute_app_path data[:caps][:app]
+ end
- # ensure app path is resolved correctly from the context of the .txt file
- ENV['APP_PATH'] = Appium::Driver.absolute_app_path ENV['APP_PATH']
- 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]
+ # ensure files are absolute
+ r.map! do |file|
+ file = File.exists?(file) ? file :
+ File.join(parent_dir, file)
+ file = File.expand_path file
- # return list of require files as an array
- # nil if require doesn't exist
- if data && data['require']
- r = data['require']
- r = r.kind_of?(Array) ? r : [ r ]
- # ensure files are absolute
- r.map! do |file|
- file = file.include?(File::Separator) ? file :
- File.join(parent_dir, file)
- file = File.expand_path file
+ File.exists?(file) ? file : nil
+ end
+ r.compact! # remove nils
- File.exists?(file) ? file : nil
- end
- r.compact! # remove nils
+ files = []
- files = []
-
- # now expand dirs
- r.each do |item|
- unless File.directory? item
- # save file
- files << item
- next # only look inside folders
+ # now expand dirs
+ r.each do |item|
+ 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|
+ # do not add folders to the file list
+ files << File.expand_path(file) unless File.directory? file
+ end
end
- Dir.glob(File.join(item, '**/*.rb')) do |file|
- # do not add folders to the file list
- files << File.expand_path(file) unless File.directory? file
- end
+
+ # Must not sort files. File order is specified in appium.txt
+ data[:appium_lib][:require] = files
end
- files
+ data
end
-end
-# Fix uninitialized constant Minitest (NameError)
-module Minitest
- # Fix superclass mismatch for class Spec
- class Runnable; end
- class Test < Runnable; end
- class Spec < Test; end
-end
+ # 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
+ result = {}
+ hash.each do |key, value|
+ key = key.to_sym rescue key
+ result[key] = value.is_a?(Hash) ? symbolize_keys(value) : value
+ end
+ result
+ end
-module Appium
- add_to_path __FILE__
-
- require 'selenium-webdriver'
-
- # common
- require_relative 'common/helper'
- require_relative 'common/patch'
- require_relative 'common/version'
- require_relative 'common/element/button'
- require_relative 'common/element/text'
- require_relative 'common/element/window'
-
- # ios
- require_relative 'ios/helper'
- require_relative 'ios/patch'
- require_relative 'ios/element/alert'
- require_relative 'ios/element/generic'
- require_relative 'ios/element/textfield'
-
- # android
- require_relative 'android/dynamic'
- require_relative 'android/helper'
- require_relative 'android/patch'
- require_relative 'android/element/alert'
- require_relative 'android/element/generic'
- require_relative 'android/element/textfield'
-
def self.promote_singleton_appium_methods main_module
raise 'Driver is nil' if $driver.nil?
main_module.constants.each do |sub_module|
#noinspection RubyResolve
$driver.public_methods(false).each do |m|
@@ -169,11 +175,10 @@
# 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?
# Wrap single class into an array
class_array = [class_array] unless class_array.class == Array
# Promote Appium driver methods to class instance methods.
@@ -183,13 +188,13 @@
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
@@ -199,118 +204,70 @@
end
class Driver
@@loaded = false
- attr_reader :default_wait, :app_path, :app_name, :device,
- :app_package, :app_activity, :app_wait_activity,
- :sauce_username, :sauce_access_key, :port, :debug,
- :export_session, :device_cap, :compress_xml, :custom_url
+ # attr readers are promoted to global scope. To avoid clobbering, they're
+ # made available via the driver_attributes method
# The amount to sleep in seconds before every webdriver http call.
attr_accessor :global_webdriver_http_sleep
- # Creates a new driver.
- # :device is :android, :ios, or :selendroid
+
+ # Creates a new driver
#
# ```ruby
- # # Options include:
- # :app_path, :app_name, :app_package, :app_activity,
- # :app_wait_activity, :sauce_username, :sauce_access_key,
- # :port, :os, :debug
- #
# require 'rubygems'
# require 'appium_lib'
#
+ # # platformName takes a string or a symbol.
+ #
# # Start iOS driver
- # app = { device: :ios, app_path: '/path/to/MyiOS.app'}
- # Appium::Driver.new(app).start_driver
+ # opts = { caps: { platformName: :ios, app: '/path/to/MyiOS.app' } }
+ # Appium::Driver.new(opts).start_driver
#
# # Start Android driver
- # apk = { device: :android
- # app_path: '/path/to/the.apk',
- # app_package: 'com.example.pkg',
- # app_activity: 'act.Start',
- # app_wait_activity: 'act.Start'
- # }
- #
+ # opts = { caps: { platformName: :android, app: '/path/to/my.apk' } }
# Appium::Driver.new(apk).start_driver
# ```
#
# @param opts [Object] A hash containing various options.
# @return [Driver]
def initialize opts={}
# quit last driver
$driver.driver_quit if $driver
- opts = {} if opts.nil?
- tmp_opts = {}
+ raise 'opts must be a hash' unless opts.kind_of? Hash
- # convert to downcased symbols
- opts.each_pair { |k,v| tmp_opts[k.to_s.downcase.strip.intern] = v }
- opts = tmp_opts
+ opts = Appium::symbolize_keys opts
- @raw_capabilities = opts.fetch(:raw, {})
+ # default to {} to prevent nil.fetch and other nil errors
+ @caps = opts[:caps] || {}
+ appium_lib_opts = opts[:appium_lib] || {}
- @custom_url = opts.fetch :server_url, false
+ # 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
+ @last_waits = [@default_wait]
+ @sauce_username = appium_lib_opts.fetch :sauce_username, ENV['SAUCE_USERNAME']
+ @sauce_access_key = appium_lib_opts.fetch :sauce_access_key, ENV['SAUCE_ACCESS_KEY']
+ @port = appium_lib_opts.fetch :port, 4723
- @compress_xml = opts[:compress_xml] ? true : false
-
- @export_session = opts.fetch :export_session, false
-
- @default_wait = opts.fetch :wait, 30
- @last_waits = [@default_wait]
-
# Path to the .apk, .app or .app.zip.
# The path can be local or remote for Sauce.
- @app_path = opts.fetch :app_path, ENV['APP_PATH']
- raise 'APP_PATH must be set.' if @app_path.nil?
+ unless !@caps || @caps[:app].nil? || @caps[:app].empty?
+ @caps[:app] = self.class.absolute_app_path @caps[:app]
+ end
- # The name to use for the test run on Sauce.
- @app_name = opts.fetch :app_name, ENV['APP_NAME']
+ # https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile
+ @device = @caps[:platformName]
+ @device = @device.is_a?(Symbol) ? @device : @device.downcase.strip.intern if @device
+ raise "platformName must be set. Not found in options: #{opts}" unless @device
+ raise 'platformName must be Android or iOS' unless [:android, :ios].include?(@device)
- # Android app package
- @app_package = opts.fetch :app_package, ENV['APP_PACKAGE']
-
- # Android app starting activity.
- @app_activity = opts.fetch :app_activity, ENV['APP_ACTIVITY']
-
- # Android app waiting activity
- @app_wait_activity = opts.fetch :app_wait_activity, ENV['APP_WAIT_ACTIVITY']
-
- @android_coverage = opts.fetch :android_coverage, ENV['ANDROID_COVERAGE']
- # init to empty hash because it's merged later as an optional desired cap.
- @android_coverage = @android_coverage ? { androidCoverage: @android_coverage} : {}
-
- # Sauce Username
- @sauce_username = opts.fetch :sauce_username, ENV['SAUCE_USERNAME']
-
- # Sauce Key
- @sauce_access_key = opts.fetch :sauce_access_key, ENV['SAUCE_ACCESS_KEY']
-
- @port = opts.fetch :port, ENV['PORT'] || 4723
-
- # 'iPhone Simulator'
- # 'iPad Simulator'
- # 'Android'
- # 'Selendroid'
- #
- # :ios, :android, :selendroid
- @device = opts.fetch :device, ENV['DEVICE']
- raise 'Device must be set' unless @device
-
- @device_type = opts.fetch :device_type, 'tablet'
- @device_orientation = opts.fetch :device_orientation, 'portrait'
-
- @full_reset = opts.fetch :full_reset, true
- @no_reset = opts.fetch :no_reset, false
-
- # no_reset/full_reset are mutually exclusive
- @no_reset = false if @full_reset
- @full_reset = false if @no_reset
-
# load common methods
extend Appium::Common
- if @device.downcase == 'android'
+ if device_is_android?
# load Android specific methods
extend Appium::Android
else
# load iOS specific methods
extend Appium::Ios
@@ -319,97 +276,78 @@
# apply os specific patches
patch_webdriver_element
# enable debug patch
# !!'constant' == true
- @debug = opts.fetch :debug, !!defined?(Pry)
+ @debug = appium_lib_opts.fetch :debug, !!defined?(Pry)
puts "Debug is: #{@debug}"
if @debug
ap opts unless opts.empty?
puts "Device is: #{@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
+ # load device methods exactly once
+ extend Appium::Device
+
# Promote only on Minitest::Spec (minitest 5) by default
Appium.promote_appium_methods ::Minitest::Spec
end
self # return newly created driver
- end # def initialize
-
- # Returns the status payload
- #
- # ```ruby
- # {"status"=>0,
- # "value"=>
- # {"build"=>
- # {"version"=>"0.8.2",
- # "revision"=>"f2a2bc3782e4b0370d97a097d7e04913cf008995"}},
- # "sessionId"=>"8f4b34a7-a9a9-4ac5-b125-36258143446a"}
- # ```
- #
- # Discover the Appium rev running on the server.
- #
- # `status["value"]["build"]["revision"]`
- # `f2a2bc3782e4b0370d97a097d7e04913cf008995`
- #
- # @return [JSON]
- def status
- driver.status.payload
end
- # Returns the server's version string
- # @return [String]
- def server_version
- status['value']['build']['version']
- end
+ # Returns a hash of the driver attributes
+ def driver_attributes
+ attributes = { caps: @caps,
+ custom_url: @custom_url,
+ export_session: @export_session,
+ default_wait: @default_wait,
+ last_waits: @last_waits,
+ sauce_username: @sauce_username,
+ sauce_access_key: @sauce_access_key,
+ port: @port,
+ device: @device,
+ debug: @debug,
+ }
- # @private
- # WebDriver capabilities. Must be valid for Sauce to work.
- # https://github.com/jlipps/appium/blob/master/app/android.js
- def android_capabilities
- {
- compressXml: @compress_xml,
- platform: 'Linux',
- platformName: @device,
- fullReset: @full_reset,
- noReset: @no_reset,
- :'device-type' => @device_type,
- :'device-orientation' => @device_orientation,
- name: @app_name || 'Ruby Console Android Appium',
- :'app-package' => @app_package,
- :'app-activity' => @app_activity,
- :'app-wait-activity' => @app_wait_activity || @app_activity,
- }.merge(@android_coverage).merge(@raw_capabilities)
+ # Return duplicates so attributes are immutable
+ attributes.each do |key, value|
+ attributes[key] = value.duplicable? ? value.dup : value
+ end
+ attributes
end
- # @private
- # WebDriver capabilities. Must be valid for Sauce to work.
- def ios_capabilities
- {
- platform: 'OS X 10.9',
- platformName: @device,
- name: @app_name || 'Ruby Console iOS Appium',
- :'device-orientation' => @device_orientation
- }.merge(@raw_capabilities)
+ def device_is_android?
+ @device == :android
end
- # @private
- def capabilities
- caps = @device.downcase === 'android' ? android_capabilities : ios_capabilities
- caps[:app] = self.class.absolute_app_path(@app_path) unless @app_path.nil? || @app_path.empty?
- caps
+ # Returns the server's version info
+ #
+ # ```ruby
+ # {
+ # "build" => {
+ # "version" => "0.18.1",
+ # "revision" => "d242ebcfd92046a974347ccc3a28f0e898595198"
+ # }
+ # }
+ # ```
+ #
+ # @return [Hash]
+ def appium_server_version
+ driver.remote_status
end
- # Converts environment variable APP_PATH to an absolute path.
+ # Converts app_path to an absolute path.
# @return [String] APP_PATH as an absolute path
def self.absolute_app_path app_path
raise 'APP_PATH 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:'
@@ -429,11 +367,11 @@
app_path = File.expand_path app_path
raise "App doesn't exist #{app_path}" unless File.exist? app_path
app_path
end
- # Get the server url for sauce or local based on env vars.
+ # Get the server url
# @return [String] the server url
def server_url
return @custom_url if @custom_url
if !@sauce_username.nil? && !@sauce_access_key.nil?
"http://#{@sauce_username}:#{@sauce_access_key}@ondemand.saucelabs.com:80/wd/hub"
@@ -467,54 +405,46 @@
end
# Quits the driver
# @return [void]
def driver_quit
- # rescue NoSuchDriverError
- begin; @driver.quit unless @driver.nil?; rescue; end
+ # rescue NoSuchDriverError or nil driver
+ @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 = @client || Selenium::WebDriver::Remote::Http::Default.new
@client.timeout = 999999
begin
- @driver = Selenium::WebDriver.for :remote, http_client: @client, desired_capabilities: capabilities, url: server_url
- # Load touch methods. Required for Selendroid.
+ @driver = Selenium::WebDriver.for :remote, http_client: @client, desired_capabilities: @caps, url: server_url
+ # Load touch methods.
@driver.extend Selenium::WebDriver::DriverExtensions::HasTouchScreen
# export session
if @export_session
- begin
- File.open('/tmp/appium_lib_session', 'w') do |f|
- f.puts @driver.session_id
- end
- rescue
- end
+ File.open('/tmp/appium_lib_session', 'w') do |f|
+ f.puts @driver.session_id
+ end rescue nil
end
rescue Errno::ECONNREFUSED
raise 'ERROR: Unable to connect to Appium. Is the server running?'
end
- # Set timeout to a large number so that Appium doesn't quit
- # when no commands are entered after 60 seconds.
- # broken on selendroid: https://github.com/appium/appium/issues/513
- mobile :setCommandTimeout, timeout: 9999 unless @device == 'Selendroid'
-
# Set implicit wait by default unless we're using Pry.
@driver.manage.timeouts.implicit_wait = @default_wait unless defined? Pry
@driver
end
# Set implicit wait and default_wait to zero.
def no_wait
- @last_waits = [@default_wait, 0]
- @default_wait = 0
+ @last_waits = [@default_wait, 0]
+ @default_wait = 0
@driver.manage.timeouts.implicit_wait = 0
end
# Set implicit wait and default_wait to timeout, defaults to 30.
# if set_wait is called without a param then the second to last
@@ -535,11 +465,11 @@
# puts "timeout = @default_wait = #{@last_waits}"
timeout = @default_wait = @last_waits.first
else
@default_wait = timeout
# puts "last waits before: #{@last_waits}"
- @last_waits = [@last_waits.last, @default_wait]
+ @last_waits = [@last_waits.last, @default_wait]
# puts "last waits after: #{@last_waits}"
end
@driver.manage.timeouts.implicit_wait = timeout
end
@@ -567,11 +497,11 @@
# 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
+ exists = true
begin
search_block.call # search for element
rescue
exists = false # error means it's not there
@@ -589,29 +519,10 @@
# @return [Object]
def execute_script script, *args
@driver.execute_script script, *args
end
- # Helper method for mobile gestures
- #
- # https://github.com/appium/appium/wiki/Automating-mobile-gestures
- #
- # driver.execute_script 'mobile: swipe', endX: 100, endY: 100, duration: 0.01
- #
- # becomes
- #
- # mobile :swipe, endX: 100, endY: 100, duration: 0.01
- # @param method [String, Symbol] the method to execute
- # @param args [*args] the args to pass to the method
- # @return [Object]
- def mobile method, *args
- raise 'Method must not be nil' if method.nil?
- raise 'Method must have .to_s' unless method.respond_to? :to_s
-
- @driver.execute_script "mobile: #{method.to_s}", *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
@@ -631,12 +542,12 @@
# @return [void]
def x
driver_quit
exit # exit pry
end
- end # end class Driver
-end # end module Appium
+ end # class Driver
+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)