UiAutomatorViewer是Android SDK自带的测试工具,用来查看手机或模拟器上的界面元素,小巧,简单,开箱即用,十分方便。美中不足之处在于,它不能获取界面元素的xpath.
写自动化测试脚本时,xpath是一种非常方便的定位方式。Appium等一些成熟的工具框架可以获取到界面元素xpath,但使用起来稍有点重量级。那么是否也可以给UiAutomatorViewer添加xpath支持呢?
答案是肯定的。
首先下载UiAutomatorView源代码,我用的地址是https://android.googlesource.com/platform/frameworks/testing/+/aecdc4a/uiautomator/utils/
代码不多,可以直接组织成一个Java项目,快速上手。如下图,其中image目录在下方,没有列出。
入口类是com.android.uiautomator.UiAutomatorViewer,没什么好说的,不用修改。
主要关注的类是com.android.uiautomator.tree.UiHierarchyXmlLoader,重点是以下代码片断
DefaultHandler handler = new DefaultHandler(){ BasicTreeNode mParentNode; BasicTreeNode mWorkingNode; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { boolean nodeCreated = false; // starting an element implies that the element that has not yet been closed // will be the parent of the element that is being started here mParentNode = mWorkingNode; if ("hierarchy".equals(qName)) { mWorkingNode = new RootWindowNode(attributes.getValue("windowName")); nodeCreated = true; } else if ("node".equals(qName)) { UiNode tmpNode = new UiNode(); for (int i = 0; i < attributes.getLength(); i++) { tmpNode.addAtrribute(attributes.getQName(i), attributes.getValue(i)); }
UiAutomatorViewer调用安卓命令,生成当前界面的XML,这段代码用来解析XML,生成控件树。hierarchy是根节点,其余节点名称都是node,接下来的for循环里把node的属性存入控件节点。
生成xpath的原理:
1. 若节点resource-id属性非空且唯一,则xpath为 //CLASS[@resource-id=‘...‘],其中CLASS就是节点的class属性,比如android.widget.TextView
2. 若节点text属性非空且唯一,则xpath为 //CLASS[@text=‘...‘]
3. 若节点content-desc属性非空且唯一,则xpath为 //CLASS[@content-desc=‘...‘]
4. 若以上三属性都不唯一,则尝试它们的两两组合是否唯一,比如resource-id和text组合唯一,则xpath为 //CLASS[@resource-id=‘...‘ and @text=‘...‘]
5. 三属性两两组合不唯一,则尝试三者组合是否唯一
6. 以上条件全不满足,则先获取父节点的xpath,再把当前节点拼上去,如://android.widget.ListView[@resource-id=‘android:id/list‘]/android.widget.LinearLayout[6],其中android.widget.LinearLayout[6]代表当前节点,前面的部分则是父节点。这个过程需要以层序遍历控件树,这样当处理一个节点时,可以保证它的父节点xpath已生成完毕。
有一个细节需要注意,安卓设备生成的界面XML中,每个节点有一个index属性,表示它作为父节点的第几个子节点,编号从0开始。需要注意的是,这个index不区分子节点类型。比如,一个父节点有6个子节点,那么无论它们是什么类型,index属性只与它们的次序有关。
但在xpath中确不是这样,比如前面第6条中的例子,//android.widget.ListView[@resource-id=‘android:id/list‘]/android.widget.LinearLayout[6],它表示的是第6个类型为android.widget.LinearLayout的子节点,也就是说xpath中的序号是区分类型的。
为了处理这个问题,我在第一版的实现中,把xpath按这样生成://android.widget.ListView[@resource-id=‘android:id/list‘]/*[6], 也就是说,忽略子节点类型,直接获取第6个,这个序号就是节点的index属性再加1,因为xpath是从1开始编号,而控件树index属性从0开始。
一般情况下,这是可以的,但有少数情况,控件树的index属性不连续,比如第一个子节点index为0,第二个子节点index为2,1被跳过去了。那么当我还是按照前面方法生成xpath时,第二个节点就找不到了。
解决办法还是有的,当一个节点作为子节点被添加到父节点时,检查前面已经添加过几个同类型的节点,根据这个信息,确定自己的编号。为了与index区分,这个“编号”属性取名为classIndex,表示同类型子节点中的序号。比如,现在一个类型为android.widget.LinearLayout的节点被添加到父节点,发现父节点已经有了两个该类型的子节点,同时还有其他类型子节点若干,那么这个新添加的节点编号就是3,即classIndex=3
生成xpath的过程应该在整个XML解析完毕之后再开始,从根节点出发,按层序遍历节点生成xpath和classIndex,并调用UiNode.addAttribute(key, value)添加到节点中。需要注意的是根节点的xpath为:/hierarchy
大功告成,启动程序测试一下效果吧。如下图,右下角即是控件元素对应的xpath