最近 Q 群上有位同学遇到难题,它的 hybrid app 无论通过 selendroid 还是 appium 都无法完成登录及登录后界面元素的点击操作。所以我大致探究了一下,确实需要一定的特别办法来解决这些问题。

问题描述

1、chromedriver 无法触发 webview 中登录按钮的 tap 事件
2、使用坐标暴力登录后,dev tool 里出现了多个窗口,通过 getPageSource 只能获取到部分源码

问题分析

1、chromedriver 无法触发 webview 中登录按钮的 tap 事件

首先查文档,发现两个关键 issue:https://github.com/appium/appium/issues/3434https://code.google.com/p/chromedriver/issues/detail?id=635。大致意思都是 chromedriver 目前还不能触发 touch action(swipe, tap, flick 等)。appium 的解决方案是在 chrome app 中通过切换回 NATIVE_APP 用 uiautomator 来触发 tap 事件。不过只能对 chrome app 有效(web app),对于 hybrid app 无效。

2、dev tools 里出现多个窗口

根据这位同学提供的截图,多个窗口都是在同一个应用中的,不同窗口的 url 都不一样,所以初步分析这多个窗口实质上是多个 windows。

解决思路

1、chromedriver 无法触发 webview 中登录按钮的 tap 事件

既然 appium 在 web app 中解决方案是切回 NATIVE_APP,那么我们也能在 hybrid app 中仿照这种方式进行。且通过 uiautomatorviewer 可以看到在 dump 出来的 xml 中存在 webview 里面的节点(其中登录按钮的 content-desc 的值为'登录'),所以通过 accessibility id 应该能找到。

结果用 accessibility id 找不到,xpath 也找不到。用 getPageSource 获取 dump 的源码,发现 webview 里面的元素没有映射出来。。。

后面经过恒温大哥的提醒,尝试了一下http://testerhome.com/topics/1047的方法,发现用adb shell uiautomator dump /data/local/tmp/uidump.xml dump 出来的 xml 里面含有 webview 内部元素节点。接下来就简单了,改写一下帖子里面的代码,增加一个方法,就能通过 content-desc 来获取元素坐标了。

2、dev tools 里出现多个窗口

这一个没有探究太多,因为用self.driver.window_handles对比一下就已经知道登陆后的 window handle 数量增加了。至于为啥一个界面里面会有这么多个 window 且都不含 frame?留待后面探究它的 js 源码了。

最终代码

时间关系,写得比较龊,大家轻拍。。。

# coding=utf-8

import os
from time import sleep

import unittest
import tempfile
import os
import re
import time
import xml.etree.cElementTree as ET

from appium import webdriver


# Returns abs path relative to this file and not cwd
PATH = lambda p: os.path.abspath(
    os.path.join(os.path.dirname(__file__), p)
)


class Element(object):
    """
    通过元素定位,需要Android 4.0以上
    参考资料: http://testerhome.com/topics/1047
    """
    def __init__(self):
        """
        初始化,获取系统临时文件存储目录,定义匹配数字模式
        """
        self.tempFile = tempfile.gettempdir()
        self.pattern = re.compile(r"\d+")

    def __uidump(self):
        """
        获取当前Activity控件树
        """
        os.popen("adb shell uiautomator dump /data/local/tmp/uidump.xml")
        os.popen("adb pull /data/local/tmp/uidump.xml " + self.tempFile)

    def __element(self, attrib, name):
        """
        同属性单个元素,返回单个坐标元组
        """
        self.__uidump()
        tree = ET.ElementTree(file=os.path.join(self.tempFile, "uidump.xml"))
        treeIter = tree.iter(tag="node")
        for elem in treeIter:
            if elem.attrib[attrib] == name:
                bounds = elem.attrib["bounds"]
                coord = self.pattern.findall(bounds)
                Xpoint = (int(coord[2]) - int(coord[0])) / 2.0 + int(coord[0])
                Ypoint = (int(coord[3]) - int(coord[1])) / 2.0 + int(coord[1])

                return Xpoint, Ypoint


    def __elements(self, attrib, name):
        """
        同属性多个元素,返回坐标元组列表
        """
        list = []
        self.__uidump()
        tree = ET.ElementTree(file=os.path.join(self.tempFile, "uidump.xml"))
        treeIter = tree.iter(tag="node")
        for elem in treeIter:
            if elem.attrib[attrib] == name:
                bounds = elem.attrib["bounds"]
                coord = self.pattern.findall(bounds)
                Xpoint = (int(coord[2]) - int(coord[0])) / 2.0 + int(coord[0])
                Ypoint = (int(coord[3]) - int(coord[1])) / 2.0 + int(coord[1])
                list.append((Xpoint, Ypoint))
        return list

    def find_element_by_name(self, name):
        """
        通过元素名称定位
        usage: find_element_by_name(u"相机")
        """
        return self.__element("text", name)

    def find_elements_by_name(self, name):
        return self.__elements("text", name)

    def find_element_by_class(self, className):
        """
        通过元素类名定位
        usage: find_element_by_class("android.widget.TextView")
        """
        return self.__element("class", className)

    def find_elements_by_class(self, className):
        return self.__elements("class", className)

    def find_element_by_id(self, id):
        """
        通过元素的resource-id定位
        usage: find_element_by_id("com.android.deskclock:id/imageview")
        """
        return self.__element("resource-id",id)

    def find_elements_by_id(self, id):
        return self.__elements("resource-id",id)

    def find_element_by_content_desc(self, content_desc):
        """
        find by content description
        usage: find_element_by_content_desc(u"content-desc value")
        """
        return self.__element("content-desc", content_desc)


class SimpleAndroidTests(unittest.TestCase):
    def setUp(self):
        desired_caps = {}
        desired_caps['platformName'] = 'Android'
        desired_caps['platformVersion'] = '4.4'
        desired_caps['deviceName'] = 'Android Emulator'
        desired_caps['app'] = PATH(
            'H59F974FE_0327183043.apk'
        )

        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

    def tearDown(self):
        # end the session
        self.driver.quit()

    def test_login_and_get_windows(self):
        print "available contexts are %s" % self.driver.contexts
        print "current context is %s" % self.driver.current_context

        time.sleep(5)
        print "switch to context {}".format(u"WEBVIEW_io.dcloud.shang")
        self.driver.switch_to.context(u"WEBVIEW_io.dcloud.shang")

        # input username and password using chromedriver
        self.driver.find_element_by_id("username").send_keys("jiangwei")
        self.driver.find_element_by_id("password").send_keys("123456")
        print "window_handlers before login: {}".format(self.driver.window_handles)

        # click login button using uiautomator
        self.driver.switch_to.context(u"NATIVE_APP")
        element_locator = Element()
        x, y = element_locator.find_element_by_content_desc(u"登录")
        login_button_location = [int(x), int(y)]
        print "login button location in dump file: {}".format(login_button_location)
        self.driver.tap([login_button_location], duration=100)
        # wait for login
        time.sleep(10)

        # get all windows' titles using chromedriver
        self.driver.switch_to.context(u"WEBVIEW_io.dcloud.shang")
        print "window_handlers after login: {}".format(self.driver.window_handles)
        for window in self.driver.window_handles:
            self.driver.switch_to_window(window)
            try:
                print "{} title: {}".format(self.driver.current_window_handle, self.driver.title)
            except Exception:
                pass

if __name__ == '__main__':
    suite = unittest.TestLoader().loadTestsFromTestCase(SimpleAndroidTests)
    unittest.TextTestRunner(verbosity=2).run(suite)


↙↙↙阅读原文可查看相关链接,并与作者交流