最近 Q 群上有位同学遇到难题,它的 hybrid app 无论通过 selendroid 还是 appium 都无法完成登录及登录后界面元素的点击操作。所以我大致探究了一下,确实需要一定的特别办法来解决这些问题。
1、chromedriver 无法触发 webview 中登录按钮的 tap 事件
2、使用坐标暴力登录后,dev tool 里出现了多个窗口,通过 getPageSource 只能获取到部分源码
1、chromedriver 无法触发 webview 中登录按钮的 tap 事件
首先查文档,发现两个关键 issue:https://github.com/appium/appium/issues/3434,https://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)