Appium hybrid app 测试遇到的问题解决过程小结

chenhengjie123 · 发布于 2015年04月02日 · 最后由 xingopq 回复于 2016年01月06日 · 892 次阅读
本帖已被设为精华帖!

最近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)
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 9 条回复
110
driver.context("NATIVE_APP");
WebElement e = driver.findElementByAccessibilityId("登录");
driver.tap(1, e, 2);

不过我登陆之后的 context handles 还真是只有两个。。。

110

被q主给坑了,

System.out.println(driver.getWindowHandles());
  System.out.println(driver.getContextHandles());

这两个搞混了。。。

3楼 已删除
784

@chenhengjie123 赞!通过content-desc的属性值定位,在这个java版本(https://github.com/gb112211/Adb-For-Test/blob/master/java/AdbForTest/src/xuxu/autotest/element/Position.java) 里面是实现了,原本以为我再python的版本里面应该也有写这个方法才对,查看了下,居然还真忘记添进去了。。=.=

605

#2楼 @lihuazhang 好郁闷为啥shell的dump能含有webview元素,bootstrap的dump却没有…………看来后面要探究一下两者到底有什么不同了。

605

#5楼 @xuxu 谢谢你的定位思路,否则还真卡死在findElementByAccessibilityId了。

1505

#5楼 @xuxu 解析uidump.xml文件后就可以把页面元素给曝露出来了吧 content-desc定位还真不知道 我试试 thx

114

顶起。
好文。
也遇到这个坑,刚解决了,来论坛搜搜看有没方案,结果恒洁姐姐发了类似的了。
多个Activity都有相同的webview,来回切换操作,当switchTo_context到webview以后,若出现无法定位且得到的page_source也为空的情况下,请注意:driver.window_handles

4698

@chenhengjie123 关于执行switch_to_window(window)再去获取driver.title,这个的运行速度跟窗口有关么?我做的也是关于多个窗口在同一混合应用中的测试,现在只做了初步设计,但是每次到转换去获取title的时候运行速度就特别慢,不知道您有没有遇到过?有没有什么好的解决办法?
我是测试新人,请多多指教~~~_^

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册