songkick/rubium-ios

View on GitHub
lib/ui_automation/element_array.rb

Summary

Maintainability
A
35 mins
Test Coverage
module UIAutomation
  # Represents objects of type UIAElementArray in the Javascript API.
  #
  # An ElementArray represents a collection of elements of a specific type within the view hierarchy.
  #
  # This class implements #each and includes the Enumerable mixin, enabling you to use it like any other
  # native Ruby collection class.
  #
  # All methods in thie class that return a UIAutomation::Element may return a specific sub-class of 
  # UIAutomation::Element, as determined by the #element_klass attribute.
  #
  # @see https://developer.apple.com/library/ios/documentation/ToolsLanguages/Reference/UIAElementArrayClassReference/
  class ElementArray < RemoteProxy
    include Enumerable

    # The proxy for the parent element
    # @note This returns the existing proxy that created this one; it does not use the UIAElement.parent() method
    # @return [UIAutomation::Element]
    attr_reader :parent

    # The window that contains this element
    # @return [UIAutomation::Window]
    attr_reader :window
    
    # The element proxy class used for each element in the array.
    # @api private
    attr_reader :element_klass

    # @api private
    def initialize(executor, remote_object, element_klass = UIAutomation::Element, parent_element = nil, window = nil)
      super executor, remote_object
      @element_klass = element_klass
      @parent = parent_element
      @window = window
    end

    def inspect
      "<RemoteProxy(#{self.class.name}) type:#{element_klass}: #{to_javascript}>"
    end
    
    ### @!group Accessing Elements    

    # Returns a proxy to the element at the given index.
    # 
    # @param [Integer] index
    # @return [UIAutomation::Element] the element proxy
    #
    def at_index(index)
      build_proxy element_klass, remote_object.object_for_subscript(index), [parent, window]
    end

    # Returns a new ElementArray containing all elements with the given name.
    # 
    # @param [String] name
    # @return [UIAutomation::ElementArray] a sub-set of the current element array
    # @see UIAElementArray.withName()
    #
    def with_name(name)
      element_array_proxy_for :withName, name
    end

    # Returns a proxy to the first element with the given name.
    # 
    # @param [String] name
    # @return [UIAutomation::Element] the element proxy
    # @see UIAElementArray.firstWithName()
    #
    def first_with_name(name)
      element_proxy_for :firstWithName, name
    end

    # Returns a new ElementArray containing all elements matching the given predicate.
    #
    # Predicates use the Apple NSPredicate syntax.
    #
    # To avoid dealing with escaping and quoting of string values, you can use template substitution
    # in your predicate string and pass in the values as a hash.
    #
    # @example Using predicate template substitution
    #     array.with_predicate("name beginswith :value", :value => 'test')
    # 
    # @param [String] template a predicate template using NSPredicate syntax
    # @param [Hash] substitutions an optional hash of template substitutions
    # @return [UIAutomation::ElementArray] a sub-set of the current element array
    # @see UIAElementArray.withPredicate()
    # @see https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Predicates/Articles/pSyntax.html
    #
    def with_predicate(template, substitutions = {})
      element_array_proxy_for :withPredicate, UIAutomation::PredicateString.new(template, substitutions).to_s
    end

    # Returns a proxy to the first element matching the specified predicate.
    #
    # Predicates use the Apple NSPredicate syntax (see #with_predicate for more information).
    #
    # @param [String] template a predicate template using NSPredicate syntax
    # @param [Hash] substitutions an optional hash of template substitutions
    # @return [UIAutomation::Element] the element proxy
    # @see #with_predicate
    # @see UIAElementArray.firstWithPredicate()
    #
    def first_with_predicate(template, substitutions = {})
      element_proxy_for :firstWithPredicate, UIAutomation::PredicateString.new(template, substitutions).to_s
    end

    # Returns a new ElementArray containing all elements with the given key/value pair.
    # 
    # @param [String] key 
    # @param [Object] value
    # @return [UIAutomation::ElementArray] a sub-set of the current element array
    # @see UIAElementArray.withValueForKey()
    #
    def with_value_for_key(key, value)
      element_array_proxy_for :withValueForKey, value, key
    end

    # Returns a new ElementArray containing all elements with the given value.
    # 
    # @param [Object] value
    # @return [UIAutomation::ElementArray] a sub-set of the current element array
    #
    def with_value(value)
      with_value_for_key(:value, value)
    end

    # Returns a proxy to the first element with the given value.
    # 
    # @param [Object] value the element value
    # @return [UIAutomation::Element] the element proxy or nil if no elements found
    # @see UIAElementArray.firstWithName()
    #
    def first_with_value(value)
      collection = with_value(value)
      collection.any? ? collection.first : nil
    end
    
    # Returns a remote proxy to a specific element within the collection by index or name.
    #
    # You can use square-bracked syntax with either an Integer or String key to perform lookup
    # by index or name respectively.
    #
    # Calling with an Integer is equivalent to calling #at_index. Calling with a String is
    # equivalent to calling #first_with_name.
    # 
    # @param [String or Integer] index_or_name the index or name of the attribute
    # @return [UIAutomation::Element] the matching element
    # @see UIAElementArray.withName()
    #
    def [](index_or_name)
      if index_or_name.is_a?(Integer)
        at_index(index_or_name)
      else
        first_with_name(index_or_name)
      end
    end
    
    ### @!endgroup

    # Returns the number of items in the array
    # @return [Integer]
    #
    def length
      fetch(:length)
    end
    alias :count :length

    # Yields a proxy to each element in the ElementArray if a block is given.
    #
    # If no block is given, this method will return an Enumerator.
    #
    # @yieldparam [UIAutomation::Element] element if block given
    # @return [Enumerator] if no block is given
    #
    def each(&block)
      if block_given?
        (0...length).each do |index|
          yield self[index]
        end
      else
        to_enum(:each)
      end
    end

    private

    def element_proxy_for(function_name, *function_args)
      proxy_for(function_name,
        function_args: function_args,
        proxy_klass: @element_klass,
        proxy_args: [parent, window]
      )
    end

    def element_array_proxy_for(function_name, *function_args)
      proxy_for(function_name,
        function_args: function_args,
        proxy_klass: UIAutomation::ElementArray,
        proxy_args: [element_klass, parent, window]
      )
    end
  end

  # @private
  class PredicateString
    def initialize(template, substitutions = {})
      @template = template
      @substitutions = substitutions
    end

    def to_s
      @substitutions.reduce(@template) do |string, (key, value)|
        string.gsub(/:#{key}/, escaped(value))
      end
    end

    private

    def escaped(value)
      if value.is_a?(String)
        "\"#{value}\""
      else
        value.to_s
      end
    end
  end
end