lib/ui_automation.rb
require 'active_support/core_ext/string/inflections'
require 'json'
module UIAutomation
# RemoteProxy acts as a proxy or facade to Appium's ability to remotely execute
# Javascript within the Instruments runtime environment.
#
# Rather than having to manual build strings of Javascript using the Apple UIAutomation
# API and executing them using Rubium::Driver#execute, you can use a RemoteProxy as
# if it was an instance of a Javascript object within the UIAutomation API.
#
# You can fetch values of properties, perform methods and obtain new proxies to objects
# returned by a Javascript method.
#
# As well as providing explicit APIs for fetching properties and performing methods,
# you can also use square-bracket notation for accessing properties and for methods,
# you can just call them on the proxy as if they were Ruby methods. You can perform
# any method that is defined in the UIAutomation API and you can also use underscore_case
# - it will automatically be converted to `lowerCamelCase`.
#
# Generally, you wouldn't use this directly but would instead use one of the sub-classes
# within the library: the Ruby mirror of the UIAutomation Javascript API is built on top
# of this class.
#
# @example Fetch the 'model' from the local target
# executor = Rubium::Driver.new(capabilities)
# target = UIAutomation::RemoteProxy.new(executor, "UIATarget.localTarget()")
# puts target.model # => 'iOS Simulator'
#
# @example Set the simulator location on the local target
# target = UIAutomation::RemoteProxy.new(executor, "UIATarget.localTarget()")
# target.set_location(lat: 90.0, lng: -10.0)
#
# @example Print the rect of the main window
# target = UIAutomation::RemoteProxy.new(executor, "UIATarget.localTarget()")
# application = target.proxy_for(:frontMostApp)
# main_window = application.proxy_for(:mainWindow)
# puts main_window.rect
#
class RemoteProxy
class << self
attr_accessor :debug_on_exception
end
# Creates a new RemoteProxy instance.
#
# Generally, the executor param will be an instance of `Rubium::Driver` but it
# can be any object that responds to `#execute(string)` and is able to execute the
# Javascript remotely using the Selenium web-driver protocol.
#
# A string of Javascript can be passed as the second parameter and it will automatically be
# converted into an instance of `RemoteJavascriptObject`.
#
# @note Use the factory methods `from_javascript` or `from_element_id` instead
# @api private
# @param [<Object#execute>] executor An object that can execute remote Javascript
# @param [String, RemoteJavascriptObject] remote_object_or_string A Javascript representation of the object to be proxied.
#
def initialize(executor, remote_object_or_string)
@executor = executor
@remote_object = remote_object_from(remote_object_or_string)
end
### @!group Factory methods
# Returns a new RemoteProxy instance from a string of Javascript.
#
# @see #initialize
#
def self.from_javascript(executor, javascript, *args)
new(executor, RemoteJavascriptObject.new(javascript), *args)
end
# Returns a new RemoteProxy instance from an Appium element ID.
#
# @note This method uses Appium's own internal element IDs returned from an XPath
# query and should not be used directly.
#
# @see #initialize
# @see Rubium::Driver#find
#
def self.from_element_id(executor, element_id, *args)
new(executor, RemoteObjectByElementID.new(element_id), *args)
end
### @!endgroup
### @!group Proxy Methods
# Returns a new proxy to the Javascript object returned from the method called on the object
# represented by self.
#
# For instance, if the current proxy represents the object `UIATarget.localTarget()` and
# you call this method with the method name :frontMostApp, it will return a proxy to the object
# represented in the Javascript API by `UIATarget.localTarget().frontMostApp()`.
#
# @param [Symbol] function_name The Javascript method name that returns the object to be proxied
# @param [Array] function_args Any arguments that should be passed to the Javascript method
# @param [Class] proxy_klass The type of RemoteProxy class to use (must be a sub-class of RemoteProxy)
# @param [Array] proxy_args Any additional arguments required to initialize a specific RemoteProxy sub-class.
# @raise `TypeError` if proxy_klass is not a valid sub-class of RemoteProxy
# @return [RemoteProxy] default return type
# @return [proxy_klass] if specified
#
def proxy_for(function_name, function_args: [], proxy_klass: RemoteProxy, proxy_args: [])
raise TypeError.new("proxy_klass must be a RemoteProxy or sub-class") unless proxy_klass <= RemoteProxy
build_proxy(proxy_klass, remote_object.object_for_function(function_name, *function_args), proxy_args)
end
# Performs a function on the current Javascript object and returns the raw value.
#
# This is useful for any methods that return values like strings, hashes and numbers.
#
# If you use this to call a method that would normally return another Javascript object
# this will simply return an empty hash. Use `#proxy_for` to return a new proxy to
# another Javascript object instead.
#
# @param [Symbol] function The name of the Javascript method to call on the object represented by self
# @param [args] args A list of arguments to be passed to the Javascript method
# @see #method_missing
#
def perform(function, *args)
@executor.execute_script(remote_object.object_for_function(function, *args).javascript)
rescue StandardError => e
binding.pry if self.class.debug_on_exception
raise "Error performing javascript: #{javascript} (server error: #{e})"
end
# Fetches the value of the named property on the current Javascript object.
#
# @param [Symbol] property The name of the property to return
# @return [Object] The Ruby equivalent of whatever the Javascript method returns.
#
def fetch(property)
@executor.execute_script(remote_object.object_for_property(property).javascript)
rescue StandardError => e
binding.pry if self.class.debug_on_exception
raise "Error performing javascript: #{javascript} (server error: #{e})"
end
# Can be used as an alternative to calling #fetch
# @see #fetch
#
def [](property)
fetch(property)
end
# @api private
def execute_self
@executor.execute_script(remote_object.javascript)
end
### @!endgroup
### @!group Debugging
# Returns the Javascript representation
# @return [String]
# @see #to_javascript
#
def to_s
to_javascript
end
def inspect
"<RemoteProxy(#{self.class.name}): #{to_javascript}>"
end
# @return [String] the Javascript representation of the proxied object
#
def to_javascript
@remote_object.javascript
end
alias :javascript :to_javascript
### @!endgroup
# Represents a remote javascript object using raw javascript, e.g.
# to represent the main application, you would initialize this with
# the string 'UIATarget.currentTarget().frontMostApp()'
#
# @api private
#
class RemoteJavascriptObject
def initialize(javascript)
@javascript = javascript
end
def javascript
@javascript
end
def to_s
javascript
end
def object_for_function(function_name, *args)
RemoteJavascriptObject.new("#{javascript}.#{function_name}(#{format_args(args)})")
end
def object_for_subscript(subscript)
RemoteJavascriptObject.new("#{javascript}[#{format_arg(subscript)}]")
end
def object_for_property(property_name)
RemoteJavascriptObject.new("#{javascript}.#{property_name}")
end
def format_arg(arg)
case arg
when String, Symbol
"'#{arg}'"
when Hash, Array
arg.to_json
else
arg
end
end
def format_args(args)
args.map { |arg| format_arg(arg) }.join(", ")
end
end
# Represents a remote javascript object by element ID, where element ID
# is the ID of a Selenium::WebDriver::Element returned by one of the built-in
# Selenium finder methods.
#
# This allows us to construct remote proxies to javascript objects that are found
# using e.g. an xpath without having to know the actual index path to the object
# in the UIAutomation javascript object tree.
#
# @api private
#
class RemoteObjectByElementID < RemoteJavascriptObject
def initialize(object_id)
@object_id = object_id
end
def javascript
# this uses internal APIs provided by appium-auto
"au.getElement(#{@object_id})"
end
end
private
def remote_object_from(remote_object_or_string)
if remote_object_or_string.is_a?(String)
RemoteJavascriptObject.new(remote_object_or_string)
elsif remote_object_or_string.is_a?(RemoteJavascriptObject)
remote_object_or_string
else
raise TypeError.new("Remote object must be a RemoteJavascriptObject or String, but was #{remote_object_or_string.class}")
end
end
# RemoteProxy lets you call methods that correspond to
# methods in the Javascript API without having to explicitly call perform().
#
# As with perform(), this should only be used for methods that return values rather
# than other objects.
#
# You can call methods in the Javascript API using snake_case or lowerCamelCase - all
# snake case methods will automatically be transformed into the Javascript lowerCamelCase
# equivalent (e.g. text_fields -> textFields).
#
# @param [Symbol] method will be converted to lowerCamelCase and used as the first argument to #perform
# @param [Array] args any arguments will be passed as arguments to the Javascript function
# @see #perform
#
def method_missing(method, *args, &block)
perform(method.to_s.camelize(:lower), *args)
end
def window
nil
end
def remote_object
@remote_object
end
def executor
@executor
end
def build_proxy(proxy_klass, remote_object, proxy_args)
proxy_klass.new(@executor, remote_object, *proxy_args)
end
end
class NoSuchElement
def method_missing(method, *args, &block)
if UIAutomation::Element.method_defined?(method)
return nil
else
warn "Tried to call #{method} on NoSuchElement"
super
end
end
end
require 'ui_automation/element_definitions'
autoload :Element, 'ui_automation/element'
autoload :ElementArray, 'ui_automation/element_array'
autoload :Application, 'ui_automation/application'
autoload :Window, 'ui_automation/window'
autoload :TableView, 'ui_automation/table_view'
autoload :TabBar, 'ui_automation/tab_bar'
autoload :NavigationBar, 'ui_automation/navigation_bar'
autoload :TextField, 'ui_automation/text_field'
autoload :Keyboard, 'ui_automation/keyboard'
autoload :Target, 'ui_automation/target'
autoload :Logger, 'ui_automation/logger'
autoload :Picker, 'ui_automation/picker'
autoload :Popover, 'ui_automation/popover'
autoload :TextView, 'ui_automation/text_view'
autoload :ActivityView, 'ui_automation/activity_view'
autoload :ActionSheet, 'ui_automation/action_sheet'
autoload :Alert, 'ui_automation/alert'
module Traits
autoload :Cancellable, 'ui_automation/traits/cancellable'
autoload :TextInput, 'ui_automation/traits/text_input'
end
end