Пример #1
0
 def find_clickable_siblings(self, e):
     info = layout.ElementInfo(e)
     if info.bounds():
         # common.debug(u'\t\tFind sibling by bounds: {}'.format(info.bounds))
         query = u"//*[@bounds='{0}']/following-sibling::*[@clickable='true']" \
                 u"| //*[@bounds='{0}']/preceding-sibling::*[@clickable='true']".format(info.bounds)
     elif info.res_id:
         # common.debug(u'\t\tFind sibling by res_id: {}'.format(info.res_id))
         query = u"//*[@resource-id='{0}']/following-sibling::*[@clickable='true']" \
                 u"| //*[@resource-id='{0}']/preceding-sibling::*[@clickable='true']".format(info.res_id)
     elif info.text:
         # common.debug(u'\t\tFind sibling by text: {}'.format(info.text))
         query = u"//*[@text='{0}']/following-sibling::*[@clickable='true']" \
                 u"| //*[@text='{0}']/preceding-sibling::*[@clickable='true']" \
                 u"| //*[text()='{0}']/following-sibling::*[@clickable='true']" \
                 u"| //*[text()='{0}']/preceding-sibling::*[@clickable='true']".format(info.text)
     elif info.desc():
         # common.debug(u'\t\tFind sibling by content_desc: {}'.format(info.desc))
         query = u"//*[@content-desc='{0}']/following-sibling::*[@clickable='true']" \
                 u"| //*[@content-desc='{0}']/preceding-sibling::*[@clickable='true']".format(info.desc)
     else:
         return []
     elements = self.driver.find_elements_by_xpath(query)
     if elements:
         return elements
     else:
         return []
Пример #2
0
 def tap_siblings(self, e):
     siblings = self.find_clickable_siblings(e)
     logger.debug(u'\t\tFind {} clickable siblings'.format(len(siblings)))
     tap_suc = False
     for s in siblings:
         ele_info = layout.ElementInfo(s)
         tap_suc = self.tap_test(s)
         if tap_suc:
             logger.debug(u'\t\tSibling click successful on {0}'.format(ele_info))
             break
     return tap_suc
Пример #3
0
    def tap_keyword(self, keyword, siblings_on=True, retry=3):
        try:
            tap_suc = False
            logger.debug(u'Try to click by keyword {0}'.format(keyword))

            # Quick try
            elements = self.find_elements_by_keyword(keyword, clickable_only=False, exact=True)
            logger.debug(u'\tQuick try, find {0} elements'.format(len(elements)))
            for e in elements:
                if self.tap_test(e):
                    return True

            # Comprehensive Try
            logger.debug(u'\tComprehensive try')
            elements = self.find_elements_by_keyword(keyword, clickable_only=False,
                                                     sort_elements=True, text_max_len=32)
            if not elements:
                logger.debug(u'\tTry scroll')
                elements = self.find_elements_by_keyword(keyword, clickable_only=False, scroll=True,
                                                         sort_elements=True, text_max_len=32)
                if not elements:
                    logger.error(u'\tCannot find keyword "{0}"'.format(keyword))
                    return False
            for e in elements:
                logger.debug(u'\tTry tapping: {0}'.format(layout.ElementInfo(e)))
                tap_suc = self.tap_test(e)
                if tap_suc:
                    break
                else:
                    if siblings_on and not e.get_attribute('clickable') == 'true':
                        logger.debug(u'\t\tTap failed, try siblings')
                        tap_suc = self.tap_siblings(e)
                        if tap_suc:
                            break
                    logger.debug(u'\tTap failed, try other matches')
            if not tap_suc:
                logger.error(u'Click by keyword "{0}" unsuccessful by all means'.format(keyword))
            return tap_suc
        except NoSuchElementException:
            if retry > 0:
                retry = retry - 1
                self.skip_irrelevant()
                return self.tap_keyword(keyword, siblings_on=siblings_on, retry=retry)
Пример #4
0
    def _scan_attempt(self, path_keywords, result_path=None, last_matched=None, result_type='attribute', retry=0):
        """One scan attempt for exploration by level-based keywords scan

        :param path_keywords: 2-D keyword list, do recursive fuzzy match
        :param result_path: list storing deterministic result path
        :param last_matched: record last matched keywords, if explore failed, remove it from path_keywords
        :param result_type: attribute - result keywords are matched attribute of elements.
                            pattern - result keywords are matched patterns defined in conf file.
        :param retry: retry flag for scrolling search
        :return: matched keyword list, or error message
        """

        # fix "default parameter is mutable" warning
        if result_path is None:
            result_path = []

        # Return condition: Every level of path_keywords is exhausted - level 0 DOES match
        #                   If dest_keywords is defined, double check for destination match
        if not path_keywords:
            if self._dest_keywords or self._dest_activities:
                if self._dest_keywords and self.wait_for_destination(self._dest_keywords, timeout=10):
                    logger.info(u'Destination keywords matched. Path: ->%s' % u'->'.join(result_path))
                    return {'status': 'success', 'path': result_path, 'package': self.package}
                elif self._dest_activities and self.wait_for_activities(self._dest_activities, timeout=10):
                    logger.info(u'Destination activities matched. Path: ->%s' % u'->'.join(result_path))
                    return {'status': 'success', 'path': result_path, 'package': self.package}
                else:
                    logger.debug('Level 0 matched, but wrong destination')
                    return {'status': 'deadend', 'last_turn': last_matched, 'path': result_path,
                            'package': self.package}
            else:
                logger.info('Level 0 keywords matched')
                return {'status': 'success', 'path': result_path, 'package': self.package}

        # 2D list: fuzzy keyword path
        level = 0
        for kwline in path_keywords:
            # self.skip_irrelevant()
            logger.debug(u'Try level {0} keywords'.format(level))
            for kw in kwline:
                elements = self.find_elements_by_keyword(kw, clickable_only=False, exact=False,
                                                         text_max_len=32, sort_elements=True)
                if elements:
                    logger.debug(u'\tMatched {0} elements'.format(len(elements)))
                    for e in elements:
                        e_info = layout.ElementInfo(e)
                        e_location = e.location
                        logger.debug(u'\t\tTap element {0}'.format(e_info))
                        if not self.tap_test(e):
                            logger.warning(u'\t\tTap failed, try next element')
                            continue
                        else:
                            last_matched = {'lv': level, 'kw': kw}
                            if result_type == 'attribute':
                                # Use matched element attribute as keyword
                                for s in [e_info.text, e_info.res_id, e_info.desc]:
                                    if re.search(kw, s, re.I):
                                        kw = s
                            logger.info(
                                u'Level {0} matched\n\tKeyword: {1}, Position: {2}'.format(level, kw, e_location))
                            result_path.append(kw)
                            # Recursion: with path_keywords level 0 - current level
                            return self._scan_attempt(path_keywords[:level], result_path=result_path,
                                                      last_matched=last_matched, result_type=result_type)
            level += 1

        # Tried all remaining levels, none matching

        # scroll to end and retry
        if retry == 0:
            # very naive scrolling
            logger.debug(u'\tNothing found, try scrolling to end ...')
            self.scroll_down(swipe=3)
            return self._scan_attempt(path_keywords, result_path=result_path,
                                      last_matched=last_matched, result_type=result_type, retry=1)
        # scrolled and retried, return result
        else:
            if last_matched is None:
                return {'status': 'failed', 'path': result_path, 'package': self.package}
            else:
                return {'status': 'deadend', 'last_turn': last_matched, 'path': result_path, 'package': self.package}
Пример #5
0
    def skip_irrelevant(self, initial=True, limit=None, detect_login=None):
        """ Skip irrelevant activities and dialogs like update and permission notifications
            Will call itself recursively until no irrelevant things found

            This function may be invoked frequently to prevent stuck
        """
        if initial:
            self.rec_count = 0
        if detect_login is None:
            detect_login = self.app_style == 'international'
        if limit is None:
            if self.SKIP_IRR_LIMIT:
                limit = self.SKIP_IRR_LIMIT
            else:
                limit = 20

        elif self.rec_count >= self.SKIP_IRR_LIMIT:
            raise myexceptions.SkipIrrelevantExceedLimit
        else:
            self.rec_count += 1

        try:
            cur_act = self.driver.current_activity

            if not self.is_in_app():
                logger.error('!!! APP not running, raise exception ...')
                raise myexceptions.AppNotRunningException

            logger.debug(u"Try to check and skip irrelevant activities {1}, current activity: {0}"
                         .format(cur_act, "(%d)" % self.rec_count if self.rec_count else ""))
            clickable = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)')
            textedit = self.driver.find_elements_by_class_name('android.widget.EditText')

            logger.verbose(u'''Found:
                        {0} clickable elements
                        {1} TextEdit elements'''.format(len(clickable), len(textedit)))

            # Nothing can be clicked, wait or swipe
            if len(clickable) == 0 \
                    and len(textedit) == 0:
                if not self.waited:
                    logger.debug(u'Seems to be in loading page, wait then try again ...')
                    timeout = 6
                    source_before = self.page_source
                    while timeout:
                        sleep(1)
                        source_after = self.page_source
                        if difflib.SequenceMatcher(None, source_before, source_after).ratio() < 0.8:
                            break
                        source_before = source_after
                        timeout -= 1
                    self.waited = 1
                elif self.waited == 1:
                    logger.debug(u'\ttap then wait ...')
                    self.driver.tap([(self.window_size['width'] / 2, self.window_size['height'] / 2)])
                    sleep(1)
                    self.waited = 2
                elif self.waited > 1:
                    logger.debug(u'\tswipe then wait ...')
                    self.swipe_left()
                    sleep(1)
                self.skip_irrelevant(initial=False, limit=limit, detect_login=detect_login)
                return

            # Welcome page
            skippable = self.contain_skip_text()
            get_sinks = lambda: self.driver.find_elements_by_xpath('//*[not(*)]')
            if not self.is_in_dialog() \
                    and ((skippable or (len(clickable) in range(1, 5) and len(textedit) == 0))
                         and len(get_sinks()) < 20):
                logger.debug(u'Seems to be in welcome page, try bypassing it')

                if detect_login and self.contain_login_text():
                    logger.debug(u'Find login keywords in welcome page, break skip_irrelevant')
                    return

                if skippable:
                    for kw in self.skipkw:
                        for e in self.find_elements_by_keyword(kw):
                            if self.tap_test(e, diff=0.98):
                                self.skip_irrelevant(initial=False, limit=limit, detect_login=detect_login)
                                return

                safe_clickable = self.find_safe_clickable_elements()
                if safe_clickable:
                    to_tap = safe_clickable[-1]
                    ele_info = layout.ElementInfo(to_tap)
                    logger.debug(u'Tapped {0}'.format(ele_info))
                    if not self.tap_test(to_tap, diff=0.98):
                        logger.warning(u'Tap failed: {0}, try swiping'.format(ele_info))
                        self.swipe_left()

                self.skip_irrelevant(initial=False, limit=limit, detect_login=detect_login)
                return

            # Dialog
            # TODO: decide cancel/ok by context
            if self.is_in_dialog() \
                    and len(clickable) in range(1, 5) \
                    and len(textedit) == 0:
                logger.debug(u'Seems to be a dialog, try bypassing it')

                if detect_login and self.contain_login_text():
                    logger.debug(u'Find login keywords in welcome page, break skip_irrelevant')
                    return

                safe_clickable = self.find_safe_clickable_elements()
                if not safe_clickable:
                    raise myexceptions.NoSafeClickableElement("Seems like a dialog that requiring update")
                source_before_tap = self.page_source
                to_tap = safe_clickable[-1]
                ele_info = layout.ElementInfo(to_tap)
                logger.debug(u'Tapped {0}'.format(ele_info))
                self.tap(to_tap)
                if self.driver.current_activity == cur_act \
                        and difflib.SequenceMatcher(None, self.page_source.lower(),
                                                    source_before_tap).ratio() > 0.95:
                    logger.warning(u'Tap failed: {0}'.format(ele_info))
                self.skip_irrelevant(initial=False, limit=limit, detect_login=detect_login)
                return

            # City list
            cities_pattern = u'(?:鞍山|安庆|安阳|安顺|北京|天津|上海|深圳|广州|成都|南京|重庆|杭州)市?'
            filtered = self.find_elements_by_keyword(cities_pattern, clickable_only=False, exact=True, scroll=False)
            if len(filtered) > 3:
                logger.debug(u'Seems to be a city select page, try bypassing it')
                to_tap = filtered[0]
                ele_info = layout.ElementInfo(to_tap)
                logger.debug(u'Tapped {0}'.format(ele_info))
                self.tap(to_tap)
                if self.driver.current_activity == cur_act:
                    logger.warning(u'Tap failed: {0}'.format(ele_info))
                self.skip_irrelevant(initial=False, limit=limit, detect_login=detect_login)
                return
        except NoSuchElementException:
            self.skip_irrelevant(initial=False, limit=limit, detect_login=detect_login)
Пример #6
0
    def find_elements_by_keyword(self, keyword, clickable_only=False, exact=False, scroll=False,
                                 text_max_len=0, sort_elements=False, use_uiautomator=True):
        """
        Find elements where keyword matches one of {text, resource id, content description}
        :param keyword: The keyword to search for
        :param clickable_only: Only return clickable elements if set to True
        :param exact: When exact is True, return only the whole word match. Otherwise return partial match as well.
        :param scroll: Scroll and search or only search current screen
        :param text_max_len: Limit maximum text length of returned elements
        :param sort_elements: Sort returned elements by text length. The shorter, the higher priority.
        :param use_uiautomator: Whether use UIAutomator or use pure xpath for element searching.
                                UIAutomator mode support regex, but cannot match other attributes like bounds.
                                Pure XPath mode doesn't support regex, but will match all attributes.
                                Also Pure XPath mode is supposed to be faster
                                    as it sends 1 request per search while UIAutomator mode sends 3.
        :return: list of element objects
        """

        # TODO: make sure no path config use these keywords anymore and remove this deprecated part
        # =========================================================
        xmax = self.window_size['width']
        ymax = self.window_size['height']
        corner_words = {
            'TOP_LEFT_CORNER': [0, 200, 0, 200],
            'TOP_RIGHT_CORNER': [xmax - 200, xmax, 0, 200],
            'BOTTOM_RIGHT_CORNER': [xmax - 200, xmax, ymax - 200, ymax],
            'BOTTOM_LEFT_CORNER': [0, 200, ymax - 200, ymax]
        }
        if keyword in corner_words:
            return self.find_clickable_elements_in_area(*corner_words[keyword])

        # ==========================================================
        # =================== Use UIAutomator ======================
        # ==========================================================
        if use_uiautomator:
            if exact:
                regex = u"(?i)^\\s*(?:{0})\\s*$".format(keyword)
            else:
                regex = u"(?i).*(?:{0}).*".format(keyword)
            queries = [
                u'new UiSelector().textMatches("{0}")'.format(regex),
                u'new UiSelector().resourceIdMatches("{0}")'.format(regex),
                u'new UiSelector().descriptionMatches("{0}")'.format(regex)
            ]
            if clickable_only:
                queries = [q + u'.clickable(true)' for q in queries]

            # TODO: find a way to search all three attributes in one scroll
            # https://android.googlesource.com/platform/frameworks/testing/+/master/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java
            if scroll:
                queries = [u'new UiScrollable(new UiSelector().scrollable(true))'
                           u'.setMaxSearchSwipes(3).scrollIntoView({0})'.format(q) for q in queries]
                elements = []
                for q in queries:
                    elements = self.driver.find_elements_by_android_uiautomator(q)
                    # short cut in scroll mode (once found, return) to prevent too many times of scrolling.
                    if elements:
                        break
            else:
                q_combined = ';'.join(queries)
                elements = self.driver.find_elements_by_android_uiautomator(q_combined)

        # ==========================================================
        # ======================= Use XPath ========================
        # ==========================================================

        # We can use one query to search for text and all attributes:
        # //*[text()[contains(,'keyword')] or @*[contains(.,'keyword')]]
        # Use translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') for case-insensitive in XPath1.0
        #
        # !!! Appium doesnt support XPath2.0, thus doesnt support matches() and lower-case()
        # Case-insensitive:
        # //*[text()[matches(.,'keyword','i')] or @*[matches(.,'keyword','i')]]
        # Clickable only:
        # //*[text()[matches(.,'keyword','i')] or @*[matches(.,'keyword','i')] and @clickable='true']

        else:
            keyword = keyword.lower()
            if exact:
                condition = u"{1}='{0}'" \
                    .format(keyword, "translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')")
            else:
                condition = u"contains({1},'{0}')" \
                    .format(keyword, "translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')")

            if clickable_only:
                query = u"//*[text()[{0}] or @*[{0}] and @clickable='true']".format(condition)
            else:
                query = u"//*[text()[{0}] or @*[{0}]]".format(condition)

            elements = self.driver.find_elements_by_xpath(query)
            if scroll:
                scrollables = self.driver.find_elements_by_xpath(u"//*[@scrollable='true']")
                if scrollables:
                    scroll_cnt = 0
                    while not elements and scroll_cnt < 3:
                        areas = [s.size['width'] * s.size['height'] for s in scrollables]
                        i_max = areas.index(max(areas))
                        to_scroll = scrollables[i_max]
                        x = to_scroll.location['x'] + to_scroll.size['width'] / 2
                        self.scroll_down(x=x)
                        scroll_cnt += 1
                        elements = self.driver.find_elements_by_xpath(query)

        # ==========================================================
        # ===================== sort elements ======================
        # ==========================================================
        if text_max_len or sort_elements:
            len_elements = [(len(layout.ElementInfo(e).text)
                             if layout.ElementInfo(e).text
                             else len(layout.ElementInfo(e).desc), e)
                            for e in elements]  # if elements have no texts, try content_desc
            if text_max_len:
                len_elements = [pair for pair in len_elements if pair[0] <= text_max_len]
            if sort_elements:
                len_elements.sort()
            elements = [e for (l, e) in len_elements]
            if elements:
                logger.verbose(u"max: {0}, sort: {1} - {2}".format(text_max_len, sort_elements,
                                                                   [p[0] for p in len_elements]))

        return elements