自动化测试之自定义UI探测器

  • 最近在写一个自动化测试软件,写tesecase的时候总要借助其他工具(比如inspect.exe)来查看一个ui元素的 automation_id、name 等,感觉很麻烦,所以想着怎么自定义实现一个UI探测器,将它集成在我写的软件中。

  • 幸运的是我找到了一个开源的工具 FlaUInspect,但是它是C#编写的,我写的自动化测试软件是基于python实现的,但是可以借鉴下是它底层是如何实现的。

    • 按住 Ctrl 就能高亮鼠标所在位置的UI元素,如下图所示
  • 查看了下源码,底层思路大概是这个样子:

    c# 复制代码
    AutomationElement? hoveredElement = _automation?.FromPoint(screenPos);
    ElementHighlighter.HighlightElement(hoveredElement, _logger);
    • 获取鼠标坐标处的ui元素,然后调用 HighlightElement 直接高亮
    • 随后看了下HighlightElement 处的代码,大体就是显示一个 winform 窗口,2秒后自动消失。

一次绕远路的尝试

  • 既然知道了原理,那么只需要完成这两步就好了:获取鼠标处元素 + 高亮显示

  • chatgpt告诉我 uiautomation 这个包有一个函数可以直接获取鼠标处的元素,还有 pywinauto 也可以

    • 我先试了 pywinauto 发现chatgpt给的是错误答案,又问了deepseek,结果deepseek也给了我错误的答案,如下图所示,左边是chatgpt给出的代码,右边是deepseek给出的方案
  • 在尝试chatgpt的代码报错之后(uia无ElementFromPoint方法),我选择了 uiautomation + 自己绘制高亮窗口的方案。

  • 根据鼠标坐标获取元素

    python 复制代码
    import uiautomation as auto
    from PyQt6.QtGui import QCursor
    
    pos = QCursor.pos()
    element = auto.ControlFromPoint(pos.x(), pos.y())
    
    print(element, element.Name, element.ControlTypeName)
  • 自己绘制高亮窗口

    python 复制代码
    import tkinter as tk
    
    def highlight_rect_with_tkinter(rect, color='red', duration=2000):
    	"""
    		rect: 一个包含(left, top, right, bottom)的元组或对象。
    		duration: 高亮显示时长(毫秒),默认3秒后自动关闭。
    		"""
    	# 创建主窗口
    	root = tk.Tk()
    	root.overrideredirect(True)  # 无标题栏边框
    	root.attributes('-topmost', True)  # 窗口置顶
    	root.attributes('-transparentcolor', 'white')  # 设置白色为透明色
    	root.geometry('{width}x{height}+{x}+{y}'.format(
    		width=rect.right - rect.left,
    		height=rect.bottom - rect.top,
    		x=rect.left,
    		y=rect.top
    	))
    
    	# 创建画布并绘制矩形框
    	canvas = tk.Canvas(root, width=rect.right - rect.left, height=rect.bottom - rect.top, highlightthickness=0)
    	canvas.pack()
    	# 在画布边缘绘制一个矩形框,可根据需要调整线条宽度
    	canvas.create_rectangle(
    		0, 0,  # 左上角坐标(相对于画布)
    		rect.right - rect.left - 1,  # 右下角X坐标(减1确保边框可见)
    		rect.bottom - rect.top - 1,  # 右下角Y坐标
    		outline=color, width=3, fill=''  # 红色边框,无填充
    	)
    
    	# 设置定时关闭
    	root.after(duration, root.destroy)
    	root.mainloop()
  • 写完之后测试了一个qt程序,很丝滑的找到了鼠标所在位置的控件,但是当我尝试去探测wpf程序的控件时,它却只能识别到整个wpf窗口,窗口中的子控件无法识别到,说明 uiautomation 是不支持 uia 的。

回归到 pywinauto

  • 前路走不通就再尝试一下 pywinauto,这次我直接贴出了它给的代码运行的报错,这次它给出了正确答案,并且还给出了高亮的方案:pywinauto 自带高亮的方法

  • 于是将它封装成一个类,构造的时候传入回调函数,想要获取元素的时候调用 start_capture 即可(捕获元素按ctrl+enter

    python 复制代码
    from pywinauto.controls.uiawrapper import UIAWrapper
    from pywinauto.uia_element_info import UIAElementInfo
    from PyQt6.QtCore import QTimer
    from PyQt6.QtGui import QCursor
    import keyboard
    
    class AutoInspect(object):
        def __init__(self, callback):
            self.callback = callback
            self.is_capture = False
            self.ctrl_down = False
            self.enter_down = False
    
            self.timer = QTimer()
            self.timer.setInterval(1000)
            self.timer.timeout.connect(self.__timeout)
            self.init()
    
        def init(self):
            keyboard.on_press_key("ctrl", lambda e: self.__on_ctrl_down(e))
            keyboard.on_release_key("ctrl", lambda e: self.__on_ctrl_up(e))
            keyboard.on_press_key("enter", lambda e: self.__on_enter_down(e))
            keyboard.on_release_key("enter", lambda e: self.__on_enter_up(e))
            keyboard.on_press_key("esc", lambda e: self.__on_esc_enter(e))
    
        def start_capture(self):
            self.is_capture = True
            self.timer.start()
    
        def __timeout(self):
    
            if not self.is_capture:
                return
            if not self.ctrl_down:
                return
            # 监控鼠标位置 并获取元素
            pos = QCursor.pos()
            element_info = UIAElementInfo.from_point(pos.x(), pos.y())
            element = UIAWrapper(element_info)
            if element is None:
                return
            if self.enter_down:
                self.__stop_capture()
                self.callback(element)
            else:
                # 高亮显示元素
                element.draw_outline(colour='red', thickness=2)
    
        def __stop_capture(self):
            self.timer.stop()
            self.is_capture = False
    
        def __on_ctrl_down(self, e):
            self.ctrl_down = True
    
        def __on_ctrl_up(self, e):
            self.ctrl_down = False
    
        def __on_enter_down(self, e):
            self.enter_down = True
    
        def __on_enter_up(self, e):
            self.enter_down = False
    
        def __on_esc_enter(self, e):
            self.__stop_capture()
    
        def __del__(self):
            del self.timer
相关推荐
爱尔兰极光2 天前
软件测试--BUG篇
bug·压力测试·测试
国家不保护废物3 天前
Vitest 学习与实践总结:在 React + TypeScript 项目中快速上手单元测试
单元测试·测试
H_unique3 天前
初识自动化测试
自动化测试·测试·web自动化测试
MegatronKing3 天前
SSL密钥协商导致抓包失败的原因分析
前端·https·测试
Apifox3 天前
如何通过抓包工具快速生成 Apifox 接口文档?
前端·后端·测试
PetterHillWater4 天前
AI浏览器Comet用户体验测试
aigc·测试
iFlow_AI4 天前
AI 驱动的代码审查与测试用例生成:iFlow CLI在提测阶段的应用实践
prompt·测试用例·测试·心流·iflow·iflowcli
Apifox10 天前
Apifox 11 月更新|AI 生成测试用例能力持续升级、JSON Body 自动补全、支持为响应组件添加描述和 Header
前端·后端·测试
MegatronKing15 天前
一个有意思的问题引起了我的反思
前端·后端·测试