属性定位
背景
传统的 UI 自动化大多都是基于浏览器的,核心是在网页的 DOM 树上查找元素,并对其进行定位操作;
Linux 桌面应用大多是采用 Qt 编写的,在 Qt 中也是从最顶层的 MainWindow
开始构建应用,所以逻辑也是一样的,Qt 应用的自动化测试同样可以通过 DOM 树(属性树)进行元素定位,我们称之为属性定位。
借助开源工具 dogtail
我们可以对元素进行获取,从而进行定位操作。dogtail
的项目介绍可以猛戳这里。
sniff(嗅探器)使用
在终端输入 sniff 启动 AT-SPI Browser
mikigo@mikigo-PC:~$ sniff
查看应用的标签
在 sniff 里面可以看到系统中已启动的应用,点击应用名称前面的三角形图标,可以展开应用的标签,所有标签以 tree 的形式展示,对应应用里面的父窗口和子窗口。
获取元素控件的标签名称
首先,为了方便查看元素控件对应的位置,建议现在上方工具栏点击 Actions
,然后勾选 Hightlight Items
,这样在 sniff 中鼠标选中元素标签的时候,应用中会有相应的光标锁定。
在 sniff 里面点击进入应用的标签 tree 中后,点击相应的元素控件,在工具下方,会展示元素控件的 Name
,这个就是标签名称。
在 tree 中有些地方是空白的或者是 Form,是因为开发人员在添加标签的时候没有添加,或者有些父窗口不需要添加,这种在实际业务中是不影响的,我们只要保证自动化测试用例中,要用到的元素都添加了标签即可。
元素操作
获取应用对象
dogtail 获取应用对象的时候,使用的是 tree 库里面的 application() 方法:
from dogtail.tree import root
app_obj = root.application('deepin-music')
app_obj就是应用的对象。
- 获取元素对象
获取元素对象,是应用对象使用child()方法:
element = app_obj.child('element_name')
我们可以通过传入元素的 Name,获取到相应元素的对象。Name 可以通过 sniff 查看。
- 获取元素对象列表:
element_list = element.children
获取到这个元素下面所有的元素列表。
这个方法适用于有些标签没有添加,但是位置是固定的,我们通过索引可以取得元素。
element_list[0]
- 对元素的操作
在获取到元素之后,我们就可以对元素进行相应的操作。
- 单击
element.click(button=1)
button 1 —>左键,2 —>滚轮,3 —>右键,默认为 1
- 双击
element.doubleClick(button=1)
- 鼠标悬停
element.point()
鼠标移动到元素中心位置
- 文本输入
element.typeText(string)
向元素对象输入字符串 ,比如输入框
- 组合键
element.keyCombo(comboString)
框架封装
代码示例:
# 详细代码请查看 src/dogtail_utils.py
class DogtailUtils:
def __init__(self, name=None, description=None):
self.name = name
self.description = description
self.obj = root.application(self.name, self.description)
def app_element(self, *args, **kwargs):
"""
获取app元素的对象
:return: 元素的对象
"""
return self.obj.child(*args, **kwargs, retry=False)
def element_center(self, element) -> tuple:
"""
获取元素的中心位置
:param element:
:return: 元素中心坐标
"""
_x, _y, _w, _h = self.app_element(element).extents
_x = _x + _w / 2
_y = _y + _h / 2
return _x, _y
框架提供的接口非常简洁,在调用时:
self.dog.element_center("播放")
这样就能获取到此元素的中心坐标。
方法明细
#!/usr/bin/env python3
# _*_ coding:utf-8 _*_
# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
# SPDX-License-Identifier: GPL-2.0-only
# pylint: disable=C0114,C0103
import re
from typing import Union
from setting.globalconfig import GlobalConfig
from src import logger
from src.cmdctl import CmdCtl
from src.custom_exception import ElementNotFound
from src.custom_exception import ApplicationStartError
try:
from src.depends.dogtail.tree import SearchError
from src.depends.dogtail.tree import root
from src.depends.dogtail.tree import predicate
from src.depends.dogtail.tree import config
from src.depends.dogtail.tree import Node
config.childrenLimit = 1000
# config.logDebugToStdOut = False
config.logDebugToFile = False
config.searchCutoffCount = 2
GlobalConfig.NO_DOGTAIL = False
except ModuleNotFoundError:
GlobalConfig.NO_DOGTAIL = True
from src.mouse_key import MouseKey
class DogtailUtils(MouseKey):
"""
通过属性进行元素定位和操作。
"""
# pylint: disable=too-many-arguments,too-many-locals,too-many-public-methods
__author__ = "Mikigo <huangmingqiang@uniontech.com>, Litao <litaoa@uniontech.com>"
def __init__(self, name=None, description=None, number=-1, check_start=True, key: dict = None):
if GlobalConfig.NO_DOGTAIL:
raise EnvironmentError("Dogtail 及其相关依赖存在问题,调用相关方法失败~")
config.logDebugToStdOut = False
self.name = name
self.description = description
try:
if name:
self.obj = root.application(self.name, self.description)
else:
self.obj = root
if number > 0:
self.obj = self.obj.findChildren(predicate.GenericPredicate(**key))[number]
except SearchError:
if check_start:
search_app = CmdCtl.run_cmd(f"ps -ef | grep {self.name}")
logger.error(search_app)
raise ApplicationStartError(self.name) from SearchError
def app_element(self, *args, **kwargs) -> Node:
"""
获取app元素的对象
:return: 元素的对象
"""
try:
element = self.obj.child(*args, **kwargs, retry=False)
logger.debug(f"{args, kwargs} 获取元素对象 <{element}>")
return element
except SearchError:
raise ElementNotFound(*args, **kwargs) from SearchError
def get_element_children_text(self, element):
element = self.app_element(element)
all_children = element.children
text = []
for i in range(len(all_children)):
text.append(all_children[i].name)
return text
def left_upper_corner_position(self, element) -> tuple:
"""
获取元素左上角的坐标
:param element: 元素名称
:return: 元素左上角坐标
"""
position = self.app_element(element).position
logger.debug(f"获取元素 {element}元素左上角坐标 {position}")
return position
def element_size(self, *args, **kwargs) -> tuple:
"""
获取元素的大小
:return: 元素大小
"""
size = self.app_element(*args, **kwargs).size
logger.debug(f"元素{args, kwargs} 的大小 {size}")
return size
def right_upper_corner_position(self, element) -> tuple:
"""
获取元素右上角的坐标
:param element: 元素名称
:return: 元素右上角坐标
"""
_x = self.left_upper_corner_position(element)[0] + self.element_size(element)[0]
_y = self.left_upper_corner_position(element)[1]
logger.debug(f"获取元素 {element}, 右上角坐标 ({_x, _y})")
return int(_x), int(_y)
def element_center(self, element) -> tuple:
"""
获取元素的中心位置
:param element:
:return: 元素中心坐标
"""
_x, _y, _w, _h = self.app_element(element).extents
_x = _x + _w / 2
_y = _y + _h / 2
logger.debug(f"获取元素中心坐标 ({_x, _y})")
return _x, _y
def element_click(self, element, button=1):
"""
元素点击
:param element: 应用的元素
:param button: 1>left,2>middle,3>right
:return: None
"""
logger.debug(
f"""{"左键" if button == 1 else f"{'右键' if button == 3 else '鼠标中健'}"} 点击元素 {element}"""
)
mouse_click = (
self.click if button == 1 else self.right_click if button == 3 else self.middle_click
)
mouse_click(*self.element_center(element))
def element_double_click(self, element):
"""
元素双击
:return: None
"""
logger.debug(f"双击元素 {element}")
self.double_click(*self.element_center(element))
def element_point(self, element):
"""
鼠标移动到元素上(位置是在元素的中心)
:param element: 应用的元素
:return: None
"""
logger.debug(f"鼠标移至元素 {element} 中心")
self.move_to(*self.element_center(element))
@staticmethod
def __evalx(expr, element, recursive):
"""evalx"""
node = re.match(".*?[^\\\\]/", expr)
if node:
name = node.group().replace("\\/", "/")[:-1]
else:
return False
if name == "*":
element = element.children
else:
element = element.findChildren(predicate.GenericPredicate(name), recursive=recursive)
return node, element
def __trace(self, element, result, expr):
if expr.startswith("//"):
name = expr[2:]
node, element = self.__evalx(name, element, recursive=True)
elif expr.startswith("/"):
name = expr[1:]
node, element = self.__evalx(name, element, recursive=False)
else:
return False
try:
next_node = name[node.end() - 1:]
if next_node != "/":
for i in element:
self.__trace(i, result, next_node)
else:
result += element
except SearchError:
raise ElementNotFound(expr) from SearchError
return result
def find_elements_by_attr(self, expr) -> Union[list, bool]:
"""
通过层级获取元素
:param expr: 元素定位 $/xx.xxx//xxx, $根节点 /当前子节点, //递归查找子节点
:return: 元素对象
"""
logger.debug(f"查找元素 expr={expr}")
if expr == "$":
return self.obj if isinstance(self.obj, list) else [self.obj]
if not expr.startswith("$"):
return False
if not expr.endswith("/") or expr.endswith(r"\/"):
expr = expr + "/"
result = self.__trace(self.obj, [], expr[1:])
logger.debug(f"元素 {result}")
return result
def find_element_by_attr(self, expr, index=0) -> Node:
"""
查找界面元素
:param expr: 匹配格式 元素定位 $/xxx//xxx, $根节点 /当前子节点, //递归查找子节点
:param index: 匹配结果索引
:return: 元素对象
"""
elements = self.find_elements_by_attr(expr)
if not elements:
raise ElementNotFound(expr)
try:
return elements[index]
except IndexError:
raise ElementNotFound(f"{expr}, index:{index}") from IndexError
def find_element_by_attr_and_click(self, expr, index=0):
self.find_element_by_attr(expr, index).click()
def find_element_by_attr_and_right_click(self, expr, index=0):
self.find_element_by_attr(expr, index).click(3)
def find_elements_to_the_end(self, ele_name):
"""
递归查找应用界面的元素(适用于查找多个同名称元素)
:param ele_name: 需要查找的元素名称
:return: 查找到的元素对象的列表
"""
eles = []
root_ele = self.obj
def recur_inter(node=None):
if not node:
node = root_ele
children = node.children
if children:
for i in children:
if i.combovalue == ele_name:
eles.append(i)
recur_inter(i)
recur_inter()
return eles