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