Skip to content
📔 阅读量:

属性定位

背景

传统的 UI 自动化大多都是基于浏览器的,核心是在网页的 DOM 树上查找元素,并对其进行定位操作;

Linux 桌面应用大多是采用 Qt 编写的,在 Qt 中也是从最顶层的 MainWindow 开始构建应用,所以逻辑也是一样的,Qt 应用的自动化测试同样可以通过 DOM 树(属性树)进行元素定位,我们称之为属性定位

借助开源工具 dogtail 我们可以对元素进行获取,从而进行定位操作。dogtail 的项目介绍可以猛戳这里

sniff(嗅探器)使用

在终端输入 sniff 启动 AT-SPI Browser

shell
mikigo@mikigo-PC:~$ sniff

查看应用的标签

在 sniff 里面可以看到系统中已启动的应用,点击应用名称前面的三角形图标,可以展开应用的标签,所有标签以 tree 的形式展示,对应应用里面的父窗口和子窗口。

获取元素控件的标签名称

首先,为了方便查看元素控件对应的位置,建议现在上方工具栏点击 Actions,然后勾选 Hightlight Items,这样在 sniff 中鼠标选中元素标签的时候,应用中会有相应的光标锁定。

在 sniff 里面点击进入应用的标签 tree 中后,点击相应的元素控件,在工具下方,会展示元素控件的 Name,这个就是标签名称。

在 tree 中有些地方是空白的或者是 Form,是因为开发人员在添加标签的时候没有添加,或者有些父窗口不需要添加,这种在实际业务中是不影响的,我们只要保证自动化测试用例中,要用到的元素都添加了标签即可。

元素操作

获取应用对象

dogtail 获取应用对象的时候,使用的是 tree 库里面的 application() 方法:

python
from dogtail.tree import root
app_obj = root.application('deepin-music')

app_obj就是应用的对象。

  • 获取元素对象

获取元素对象,是应用对象使用child()方法:

python
element = app_obj.child('element_name')

我们可以通过传入元素的 Name,获取到相应元素的对象。Name 可以通过 sniff 查看。

  • 获取元素对象列表:
python
element_list = element.children

获取到这个元素下面所有的元素列表。

这个方法适用于有些标签没有添加,但是位置是固定的,我们通过索引可以取得元素。

python
element_list[0]
  • 对元素的操作

在获取到元素之后,我们就可以对元素进行相应的操作。

  • 单击
python
element.click(button=1)

button 1 —>左键,2 —>滚轮,3 —>右键,默认为 1

  • 双击
python
element.doubleClick(button=1)
  • 鼠标悬停
python
element.point()

鼠标移动到元素中心位置

  • 文本输入
python
element.typeText(string)

向元素对象输入字符串 ,比如输入框

  • 组合键
python
element.keyCombo(comboString)

框架封装

代码示例:

python
# 详细代码请查看 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

框架提供的接口非常简洁,在调用时:

python
self.dog.element_center("播放")

这样就能获取到此元素的中心坐标。

方法明细

python
#!/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