lib/appium_lib/android/helper.rb in appium_lib-0.24.1 vs lib/appium_lib/android/helper.rb in appium_lib-1.0.0
- old
+ new
@@ -1,447 +1,278 @@
-# encoding: utf-8
-module Appium::Android
- # Returns an array of android classes that match the tag name
- # @param tag_name [String] the tag name to convert to an android class
- # @return [String]
- def tag_name_to_android tag_name
- tag_name = tag_name.to_s.downcase.strip
-
+module Appium
+ module Android
# @private
- def prefix *tags
- tags.map!{ |tag| "android.widget.#{tag}" }
- end
- # note that 'secure' is not an allowed tag name on android
- # because android can't tell what a secure textfield is
- # they're all edittexts.
+ # http://nokogiri.org/Nokogiri/XML/SAX.html
+ class AndroidElements < Nokogiri::XML::SAX::Document
+ # TODO: Support strings.xml ids
+ attr_reader :result, :keys
- # must match names in AndroidElementClassMap (Appium's Java server)
- case tag_name
- when 'abslist'
- prefix 'AbsListView'
- when 'absseek'
- prefix 'AbsSeekBar'
- when 'absspinner'
- prefix 'AbsSpinner'
- when 'absolute'
- prefix 'AbsoluteLayout'
- when 'adapterview'
- prefix 'AdapterView'
- when 'adapterviewanimator'
- prefix 'AdapterViewAnimator'
- when 'adapterviewflipper'
- prefix 'AdapterViewFlipper'
- when 'analogclock'
- prefix 'AnalogClock'
- when 'appwidgethost'
- prefix 'AppWidgetHostView'
- when 'autocomplete'
- prefix 'AutoCompleteTextView'
- when 'button'
- prefix 'Button', 'ImageButton'
- when 'breadcrumbs'
- prefix 'FragmentBreadCrumbs'
- when 'calendar'
- prefix 'CalendarView'
- when 'checkbox'
- prefix 'CheckBox'
- when 'checked'
- prefix 'CheckedTextView'
- when 'chronometer'
- prefix 'Chronometer'
- when 'compound'
- prefix 'CompoundButton'
- when 'datepicker'
- prefix 'DatePicker'
- when 'dialerfilter'
- prefix 'DialerFilter'
- when 'digitalclock'
- prefix 'DigitalClock'
- when 'drawer'
- prefix 'SlidingDrawer'
- when 'expandable'
- prefix 'ExpandableListView'
- when 'extract'
- prefix 'ExtractEditText'
- when 'fragmenttabhost'
- prefix 'FragmentTabHost'
- when 'frame'
- prefix 'FrameLayout'
- when 'gallery'
- prefix 'Gallery'
- when 'gesture'
- prefix 'GestureOverlayView'
- when 'glsurface'
- prefix 'GLSurfaceView'
- when 'grid'
- prefix 'GridView'
- when 'gridlayout'
- prefix 'GridLayout'
- when 'horizontal'
- prefix 'HorizontalScrollView'
- when 'image'
- prefix 'ImageView'
- when 'imagebutton'
- prefix 'ImageButton'
- when 'imageswitcher'
- prefix 'ImageSwitcher'
- when 'keyboard'
- prefix 'KeyboardView'
- when 'linear'
- prefix 'LinearLayout'
- when 'list'
- prefix 'ListView'
- when 'media'
- prefix 'MediaController'
- when 'mediaroutebutton'
- prefix 'MediaRouteButton'
- when 'multiautocomplete'
- prefix 'MultiAutoCompleteTextView'
- when 'numberpicker'
- prefix 'NumberPicker'
- when 'pagetabstrip'
- prefix 'PageTabStrip'
- when 'pagetitlestrip'
- prefix 'PageTitleStrip'
- when 'progress'
- prefix 'ProgressBar'
- when 'quickcontactbadge'
- prefix 'QuickContactBadge'
- when 'radio'
- prefix 'RadioButton'
- when 'radiogroup'
- prefix 'RadioGroup'
- when 'rating'
- prefix 'RatingBar'
- when 'relative'
- prefix 'RelativeLayout'
- when 'row'
- prefix 'TableRow'
- when 'rssurface'
- prefix 'RSSurfaceView'
- when 'rstexture'
- prefix 'RSTextureView'
- when 'scroll'
- prefix 'ScrollView'
- when 'search'
- prefix 'SearchView'
- when 'seek'
- prefix 'SeekBar'
- when 'space'
- prefix 'Space'
- when 'spinner'
- prefix 'Spinner'
- when 'stack'
- prefix 'StackView'
- when 'surface'
- prefix 'SurfaceView'
- when 'switch'
- prefix 'Switch'
- when 'tabhost'
- prefix 'TabHost'
- when 'tabwidget'
- prefix 'TabWidget'
- when 'table'
- prefix 'TableLayout'
- when 'text'
- prefix 'TextView'
- when 'textclock'
- prefix 'TextClock'
- when 'textswitcher'
- prefix 'TextSwitcher'
- when 'texture'
- prefix 'TextureView'
- when 'textfield'
- prefix 'EditText'
- when 'timepicker'
- prefix 'TimePicker'
- when 'toggle'
- prefix 'ToggleButton'
- when 'twolinelistitem'
- prefix 'TwoLineListItem'
- when 'view'
- 'android.view.View'
- when 'video'
- prefix 'VideoView'
- when 'viewanimator'
- prefix 'ViewAnimator'
- when 'viewflipper'
- prefix 'ViewFlipper'
- when 'viewgroup'
- prefix 'ViewGroup'
- when 'viewpager'
- prefix 'ViewPager'
- when 'viewstub'
- prefix 'ViewStub'
- when 'viewswitcher'
- prefix 'ViewSwitcher'
- when 'web'
- 'android.webkit.WebView' # WebView is not a widget
- when 'window'
- prefix 'FrameLayout'
- when 'zoom'
- prefix 'ZoomButton'
- when 'zoomcontrols'
- prefix 'ZoomControls'
- else
- raise "Invalid tag name #{tag_name}"
- end # return result of case
- end
- # Find all elements matching the attribute
- # On android, assume the attr is name (which falls back to text).
- #
- # ```ruby
- # find_eles_attr :text
- # ```
- #
- # @param tag_name [String] the tag name to search for
- # @return [Element]
- def find_eles_attr tag_name, attribute=nil
-=begin
- sel1 = [ [4, 'android.widget.Button'], [100] ]
- sel2 = [ [4, 'android.widget.ImageButton'], [100] ]
+ def filter
+ @filter
+ end
- args = [ 'all', sel1, sel2 ]
+ # convert to string to support symbols
+ def filter= value
+ # nil and false disable the filter
+ return @filter = false unless value
+ @filter = value.to_s.downcase
+ end
- mobile :find, args
-=end
- array = ['all']
+ def initialize
+ reset
+ @filter = false
+ end
- tag_name_to_android(tag_name).each do |name|
- # sel.className(name).getStringAttribute("name")
- array.push [ [4, name], [100] ]
- end
+ def reset
+ @result = ''
+ @keys = %w[text resource-id content-desc]
+ end
- mobile :find, array
- end
+ # http://nokogiri.org/Nokogiri/XML/SAX/Document.html
+ def start_element name, attrs = []
+ return if filter && !name.downcase.include?(filter)
- # Selendroid only.
- # Returns a string containing interesting elements.
- # @return [String]
- def get_selendroid_inspect
- # @private
- def run node
- r = []
+ attributes = {}
- run_internal = lambda do |node|
- if node.kind_of? Array
- node.each { |node| run_internal.call node }
- return
+ attrs.each do |key, value|
+ if keys.include?(key) && !value.empty?
+ attributes[key] = value
+ end
end
- keys = node.keys
- return if keys.empty?
+ string = ''
+ text = attributes['text']
+ desc = attributes['content-desc']
+ id = attributes['resource-id']
- obj = {}
- # name is id
- obj.merge!( { id: node['name'] } ) if keys.include?('name') && !node['name'].empty?
- obj.merge!( { text: node['value'] } ) if keys.include?('value') && !node['value'].empty?
- # label is name
- obj.merge!( { name: node['label'] } ) if keys.include?('label') && !node['label'].empty?
- obj.merge!( { class: node['type'] } ) if keys.include?('type') && !obj.empty?
- obj.merge!( { shown: node['shown'] } ) if keys.include?('shown')
+ if !text.nil? && text == desc
+ string += " text, desc: #{text}\n"
+ else
+ string += " text: #{text}\n" unless text.nil?
+ string += " desc: #{desc}\n" unless desc.nil?
+ end
+ string += " id: #{id}\n" unless id.nil?
- r.push obj if !obj.empty?
- run_internal.call node['children'] if keys.include?('children')
+ @result += "\n#{name}\n#{string}" unless attributes.empty?
end
+ end # class AndroidElements
- run_internal.call node
- r
- end
+ # Android only.
+ # Returns a string containing interesting elements.
+ # The text, content description, and id are returned.
+ # @param class_name [String] the class name to filter on.
+ # if false (default) then all classes will be inspected
+ # @return [String]
+ def get_android_inspect class_name=false
+ parser = @android_elements_parser ||= Nokogiri::XML::SAX::Parser.new(AndroidElements.new)
- json = get_source
- node = json['children']
- results = run node
+ parser.document.reset
+ parser.document.filter = class_name
+ parser.parse get_source
- out = ''
- results.each { |e|
- no_text = e[:text].nil?
- no_name = e[:name].nil? || e[:name] == 'null'
- next unless e[:shown] # skip invisible
- # Ignore elements with id only.
- next if no_text && no_name
+ parser.document.result
+ end
- out += e[:class].split('.').last + "\n"
+ # Intended for use with console.
+ # Inspects and prints the current page.
+ # @param class_name [String] the class name to filter on.
+ # if false (default) then all classes will be inspected
+ # @return [void]
+ def page class_name=false
+ puts get_android_inspect class_name
+ nil
+ end
- # name is id when using selendroid.
- # remove id/ prefix
- e[:id].sub!(/^id\//, '') if e[:id]
+ # Lists package, activity, and adb shell am start -n value for current app.
+ # Works on local host only (not remote).
+ # noinspection RubyArgCount
+ def current_app
+ line = `adb shell dumpsys window windows`.each_line.grep(/mFocusedApp/).first.strip
+ pair = line.split(' ').last.gsub('}', '').split '/'
+ pkg = pair.first
+ act = pair.last
+ OpenStruct.new(line: line,
+ package: pkg,
+ activity: act,
+ am_start: pkg + '/' + act)
+ end
- out += " class: #{e[:class]}\n"
- # id('back_button').click
- out += " id: #{e[:id]}\n" unless e[:id].nil?
- # find_element(:link_text, 'text')
- out += " text: #{e[:text]}\n" unless no_text
- # label is name. default is 'null'
- # find_element(:link_text, 'Facebook')
- out += " name: #{e[:name]}\n" unless no_name
- # out += " visible: #{e[:shown]}\n" unless e[:shown].nil?
- }
- out
- end
+ # Find by id
+ # @param id [String] the id to search for
+ # @return [Element]
+ def id id
+ value = resolve_id id
+ # If the id doesn't resolve in strings.xml then use it as is
+ # It's probably a resource id which won't be in strings.xml
+ value = id unless value
+ exact = string_visible_exact '*', value
+ contains = string_visible_contains '*', value
+ xpath "#{exact} | #{contains}"
+ end
- def get_page_class
- r = []
- run_internal = lambda do |node|
- if node.kind_of? Array
- node.each { |node| run_internal.call node }
- return
+ # Find the element of type class_name at matching index.
+ # @param class_name [String] the class name to find
+ # @param index [Integer] the index
+ # @return [Element] the found element of type class_name
+ def ele_index class_name, index
+ unless index == 'last()'
+ # XPath index starts at 1.
+ raise "#{index} is not a valid xpath index. Must be >= 1" if index <= 0
end
+ find_element :xpath, %Q(//#{class_name}[#{index}])
+ end
- keys = node.keys
- return if keys.empty?
- r.push node['@class'] if keys.include?('@class')
+ # @private
+ def string_attr_exact class_name, attr, value
+ %Q(//#{class_name}[@#{attr}='#{value}'])
+ end
- run_internal.call node['node'] if keys.include?('node')
+ # Find the first element exactly matching class and attribute value.
+ # @param class_name [String] the class name to search for
+ # @param attr [String] the attribute to inspect
+ # @param value [String] the expected value of the attribute
+ # @return [Element]
+ def find_ele_by_attr class_name, attr, value
+ @driver.find_element :xpath, string_attr_exact(class_name, attr, value)
end
- json = get_source
- run_internal.call json['hierarchy']
- res = []
- r = r.sort
- r.uniq.each do |ele|
- res.push "#{r.count(ele)}x #{ele}\n"
+ # Find all elements exactly matching class and attribute value.
+ # @param class_name [String] the class name to match
+ # @param attr [String] the attribute to compare
+ # @param value [String] the value of the attribute that the element must have
+ # @return [Array<Element>]
+ def find_eles_by_attr class_name, attr, value
+ @driver.find_elements :xpath, string_attr_exact(class_name, attr, value)
end
- count_sort = ->(one,two) { two.match(/(\d+)x/)[1].to_i <=> one.match(/(\d+)x/)[1].to_i }
- res.sort(&count_sort).join ''
- end
- # Count all classes on screen and print to stdout.
- # Useful for appium_console.
- def page_class
- puts get_page_class
- nil
- end
-
- # Android only.
- # Returns a string containing interesting elements.
- # If an element has no content desc or text, then it's not returned by this method.
- # @return [String]
- def get_android_inspect
# @private
- def run node
- r = []
+ def string_attr_include class_name, attr, value
+ %Q(//#{class_name}[contains(translate(@#{attr},'#{value.upcase}', '#{value}'), '#{value}')])
+ end
- run_internal = lambda do |node|
- if node.kind_of? Array
- node.each { |node| run_internal.call node }
- return
- end
+ # Find the first element by attribute that exactly matches value.
+ # @param class_name [String] the class name to match
+ # @param attr [String] the attribute to compare
+ # @param value [String] the value of the attribute that the element must include
+ # @return [Element] the element of type tag who's attribute includes value
+ def find_ele_by_attr_include class_name, attr, value
+ @driver.find_element :xpath, string_attr_include(class_name, attr, value)
+ end
- keys = node.keys
- return if keys.empty?
- if keys == %w(hierarchy)
- run_internal.call node['hierarchy']
- return
- end
+ # Find elements by attribute that include value.
+ # @param class_name [String] the tag name to match
+ # @param attr [String] the attribute to compare
+ # @param value [String] the value of the attribute that the element must include
+ # @return [Array<Element>] the elements of type tag who's attribute includes value
+ def find_eles_by_attr_include class_name, attr, value
+ @driver.find_elements :xpath, string_attr_include(class_name, attr, value)
+ end
- n_content = '@content-desc'
- n_text = '@text'
- n_class = '@class'
- n_resource = '@resource-id'
- n_node = 'node'
+ # Find the first element that matches class_name
+ # @param class_name [String] the tag to match
+ # @return [Element]
+ def first_ele class_name
+ # XPath index starts at 1
+ ele_index class_name, 1
+ end
- # Store the object if it has a content description, text, or resource id.
- # If it only has a class, then don't save it.
- obj = {}
- obj.merge!( { desc: node[n_content] } ) if keys.include?(n_content) && !node[n_content].empty?
- obj.merge!( { text: node[n_text] } ) if keys.include?(n_text) && !node[n_text].empty?
- obj.merge!( { resource_id: node[n_resource] } ) if keys.include?(n_resource) && !node[n_resource].empty?
- obj.merge!( { class: node[n_class] } ) if keys.include?(n_class) && !obj.empty?
+ # Find the last element that matches class_name
+ # @param class_name [String] the tag to match
+ # @return [Element]
+ def last_ele class_name
+ ele_index class_name, 'last()'
+ end
- r.push obj if !obj.empty?
- run_internal.call node[n_node] if keys.include?(n_node)
- end
+ # Find the first element of type class_name
+ #
+ # @param class_name [String] the class_name to search for
+ # @return [Element]
+ def tag class_name
+ xpath %Q(//#{class_name})
+ end
- run_internal.call node
- r
+ # Find all elements of type class_name
+ #
+ # @param class_name [String] the class_name to search for
+ # @return [Element]
+ def tags class_name
+ xpaths %Q(//#{class_name})
end
- lazy_load_strings
- json = get_source
- node = json['hierarchy']
- results = run node
+ # @private
+ # Returns a string xpath that matches the first element that contains value
+ #
+ # example: xpath_visible_contains 'UIATextField', 'sign in'
+ #
+ # @param element [String] the class name for the element
+ # @param value [String] the value to search for
+ # @return [String]
+ def string_visible_contains element, value
+ result = []
+ attributes = %w[content-desc text]
- out = ''
- results.each { |e|
- e_desc = e[:desc]
- e_text = e[:text]
- e_class = e[:class]
- e_resource_id = e[:resource_id]
- out += e_class.split('.').last + "\n"
+ value_up = value.upcase
+ value_down = value.downcase
- out += " class: #{e_class}\n"
- if e_text == e_desc
- out += " text, name: #{e_text}\n" unless e_text.nil?
- else
- out += " text: #{e_text}\n" unless e_text.nil?
- out += " name: #{e_desc}\n" unless e_desc.nil?
+ attributes.each do |attribute|
+ result << %Q(contains(translate(@#{attribute},"#{value_up}","#{value_down}"), "#{value_down}"))
end
- out += " resource_id: #{e_resource_id}\n" unless e_resource_id.nil? || e_resource_id.empty?
+ # never partial match on a resource id
+ result << %Q(@resource-id="#{value}")
- # there may be many ids with the same value.
- # output all exact matches.
- id_matches = @strings_xml.select do |key, value|
- value == e_desc || value == e_text
- end
+ result = result.join(' or ')
- if id_matches && id_matches.length > 0
- match_str = ''
- # [0] = key, [1] = value
- id_matches.each do |match|
- match_str += ' ' * 6 + "#{match[0]}\n"
- end
- out += " id: #{match_str.strip}\n"
- end
- }
- out
- end
+ "//#{element}[#{result}]"
+ end
- # Automatically detects selendroid or android.
- # Returns a string containing interesting elements.
- # @return [String]
- def get_inspect
- @device == 'Selendroid' ? get_selendroid_inspect : get_android_inspect
- end
+ # Find the first element that contains value
+ # @param element [String] the class name for the element
+ # @param value [String] the value to search for
+ # @return [Element]
+ def xpath_visible_contains element, value
+ xpath string_visible_contains element, value
+ end
- # Intended for use with console.
- # Inspects and prints the current page.
- def page
- puts get_inspect
- nil
- end
+ # Find all elements containing value
+ # @param element [String] the class name for the element
+ # @param value [String] the value to search for
+ # @return [Array<Element>]
+ def xpaths_visible_contains element, value
+ xpaths string_visible_contains element, value
+ end
- # JavaScript code from https://github.com/appium/appium/blob/master/app/android.js
- #
- # ```javascript
- # Math.round(1.0/28.0 * 28) = 1
- # ```
- #
- # We want steps to be exactly 1. If it's zero then a tap is used instead of a swipe.
- def fast_duration
- 0.0357 # 1.0/28.0
- end
+ # @private
+ # Create an xpath string to exactly match the first element with target value
+ # @param element [String] the class name for the element
+ # @param value [String] the value to search for
+ # @return [String]
+ def string_visible_exact element, value
+ result = []
+ attributes = %w[content-desc resource-id text]
- # Lists package, activity, and adb shell am start -n value for current app.
- # Works on local host only (not remote).
- def current_app
- line = `adb shell dumpsys window windows`.each_line.grep(/mFocusedApp/).first.strip
- pair = line.split(' ').last.gsub('}','').split '/'
- pkg = pair.first
- act = pair.last
- OpenStruct.new line: line,
- package: pkg,
- activity: act,
- am_start: pkg + '/' + act
- end
+ attributes.each do |attribute|
+ result << %Q(@#{attribute}="#{value}")
+ end
- # Find by id. Useful for selendroid
- # @param id [String] the id to search for
- # @return [Element]
- def id id
- lazy_load_strings
- # resource ids must include ':' and they're not contained in strings_xml
- raise "Invalid id `#{id}`" unless @strings_xml[id] || id.include?(':')
- find_element :id, id
- end
-end # module Appium::Android
+ result = result.join(' or ')
+
+ "//#{element}[#{result}]"
+ end
+
+ # Find the first element exactly matching value
+ # @param element [String] the class name for the element
+ # @param value [String] the value to search for
+ # @return [Element]
+ def xpath_visible_exact element, value
+ xpath string_visible_exact element, value
+ end
+
+ # Find all elements exactly matching value
+ # @param element [String] the class name for the element
+ # @param value [String] the value to search for
+ # @return [Element]
+ def xpaths_visible_exact element, value
+ xpaths string_visible_exact element, value
+ end
+ end # module Android
+end # module Appium
\ No newline at end of file