对于 UI 自动化测试来说,处理好元素定位问题对于提高测试设计效率显得尤为重要,本文以美团 APP 选择商品页面为例,介绍在使用 Airtest(Poco 框架)进行元素定位时可能遇到的问题以及相应的处理方式。
目标页面如下所示:
假设页面上的药品是我们的测试数据,现在需要点击 “+” 图标,添加指定药品到购物车。
通过 Airtest IDE 查看 “+” 图标元素属性:
Path from root node: [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 4, 0, 0]
Payload details:
type : android.widget.ImageView
name : com.sankuai.meituan:id/img_foodCount_add_fix
enabled : True
visible : True
resourceId : b'com.sankuai.meituan:id/img_foodCount_add_fix'
zOrders : {'global': 0, 'local': 1}
package : b'com.sankuai.meituan'
anchorPoint : [0.5, 0.5]
dismissable : False
checkable : False
scale : [1, 1]
boundsInParent : [0.10277777777777777, 0.04625]
focusable : False
touchable : True
longClickable : False
size : [0.10277777777777777, 0.04625]
pos : [0.5259259259259259, 0.37583333333333335]
focused : False
checked : False
editalbe : False
selected : False
scrollable : False
很明显,我们可以通过 name 属性获得页面上所有的 “+” 号图标构成列表,然后根据索引选择需要点击的目标,但是这种做法存在不足,随着测试数据的变化、页面布局的变化,我们不知道自己点的是哪件商品,当然也会影响到后续对于测试数据的校验。
所以,现在新的需求是添加指定的商品到购物车,下文以 “[精华] 正柴胡饮颗粒 5g*10 袋” 这件商品为例。
首先,我们得知道 Poco 框架支持通过 text 属性精确找到元素,代码如下:
item_text_obj = poco(text="[精华]正柴胡饮颗粒5g*10袋") # 找到待测商品名称对应的元素
找到的元素在 Airtest UI 树中展示如下:
同时,可以找到待点击的 “+” 号图标在 UI 树中所处的位置:
通过观察 UI 树,可以得出目标元素(target_obj)与商品文本元素 (item_text_obj) 之间的对应关系:
商品文本元素的父节点的第五个子节点的第一个子节点的子节点就是我们要点击的目标元素。
对应的完整 Poco 代码如下:
item_text_obj = poco(text="[精华]正柴胡饮颗粒5g*10袋") # 找到待测商品名称对应的元素
foo_parent = item_text_obj.parent()
foo_childs = foo_parent.child()
target_obj = foo_childs[4].child()[0].child()[0]
target_obj.click()
这段代码放到 Airtest 执行可以通过,但是很遗憾,并不会点击到加号目标,而是进入到商品详情页面。
通过打印 target_obj 属性便可发现问题出在哪里:
日志显示,根据总结的规律找到的元素 name 是 “com.sankuai.meituan:id/txt_stickyfoodList_adapter_food_price_fix”,而之前我们已经知道期望点击的元素 name 是 “com.sankuai.meituan:id/img_foodCount_add_fix”,回到 Airtest 中的 UI 树,不难找到实际点击的元素位置:
这里其实揭示了 Poco 框架一个令人相当不爽的问题,对于任一元素的子元素,我们并不能很轻易地确定其正确的索引。
甚至存在这种可能,对于 A 商品,目标元素索引是 1,换成 B 商品,索引有可能又变成 0 了,有兴趣的可以自己慢慢尝试。
# 索引错误会导致找不到元素或点错元素
# target_obj = foo_childs[4].child()[0].child()[0]
target_obj = foo_childs[4].child()[1].child()[0] # 这才是正确的索引
target_obj.click()
问题已经找到,最后说说解法。
解法一:
item_text_obj = poco(text="[精华]正柴胡饮颗粒5g*10袋") # 找到待测商品名称对应的元素
foo_parent = item_text_obj.parent()
target_obj = foo_parent.offspring('com.sankuai.meituan:id/img_foodCount_add_fix')
Poco 找子元素除了 child 方法还有 offspring 方法,支持找到指定 name 的后代元素(注意与子元素的区别),如果目标元素的 name 可以唯一确定元素,建议使用这种方式处理。
解法二:
def get_child_by_index(element_p, structure, index):
"""
根据可见的元素位置关系选择子元素
:param element_p: 父节点
:param structure: l表示元素布局为左右结构,v表示上下结构,
:param index: 序号,按从左往右,从上往下顺序取
:return:
"""
temp_list = []
element_list = []
for element_c in element_p.child(): # 遍历子节点
temp_list.append({'element': element_c, 'pos': element_c.attr('pos')}) # 将元素以及其位置POS属性构造成字典以后再存入列表
if structure == 'l': # 左右结构
temp_list.sort(key=lambda e: e['pos'][0], reverse=False) # 调用列表sort排序方法,排序的key是字典的POS键值的下标为0的值,即横坐标
elif structure == 'v': # 上下结构
temp_list.sort(key=lambda e: e['pos'][1], reverse=False) # 调用列表sort排序方法,排序的key是字典的POS键值的下标为1的值,即纵坐标
for element in temp_list:
element_list.append(element['element'])
return element_list[index]
item_text_obj = poco(text="[精华]正柴胡饮颗粒5g*10袋") # 找到待测商品名称对应的元素
foo_parent = item_text_obj.parent()
foo1 = get_child_by_index(foo_parent, 'v',4) # 从上往下分别是图片,名称,销售额等信息
foo2 = get_child_by_index(foo1, 'l', 1) # 从左往右分别是价格信息、加号按钮
target_obj= foo2.child()
target_obj.click()
解法二的思路已经写在注释里了,全文完。