在《利用 dogtail 快速进行 GUI 自动化测试》一节中,我们实现了一个最基础的自动化测试程序 - 模拟鼠标点击按钮。现在是时候更进一步了,开始访问 UI 元素中的信息。

假设,要获取按钮上的文本(例如:用于断言测试),修改之前的脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from dogtail.tree import *
import time
# 获取应用程序(根据程序名称查找)
app = root.application(appName="Sample02", description="/home/waleon/workspace/demos/build-Samples-unknown-Debug/Sample02/Sample02")
# 获取按钮(根据 accessibleName 递归查找)
button = app.child('button')
# 获取并打印按钮上的文本
print('text:', button.text)
# 模拟鼠标点击
for i in range(3):
button.click()
sleep(1)

运行之后,你会发现 print() 打印的永远是 None。这是什么情况,按钮上分明有内容“test”,但为什么打印不出来?

这是因为,按钮元素的信息没有对外公开,以至于 dogtail 无法识别出来!

不妨用 sniff 工具查看一下,定位到我们的程序并选择该按钮,点击底下的“Text”选项:

java自动化框架搭建过程 javaui自动化_UI

没有任何内容,这就说明按钮的文本是不可访问的。

1

实现可访问性

要为 UI 元素提供可访问性支持,需要使用 Qt 的 Accessibility 技术。即应实现 QAccessibleInterface,分发的形式有两种:

实现可访问的插件:子类化 QAccessiblePlugin,重新实现纯虚函数 create(),并使用 Q_PLUGIN_METADATA() 宏导出该类(如果想在运行时按需加载,则将接口作为插件分发比较方便)。

将接口编译到应用程序中:使用接口工厂 - QAccessible::InterfaceFactory(如果是静态链接,或不想增加插件的复杂性,则推荐该方式)。

下面以接口工厂为例,来实现对按钮文本的可访问性(要对文本进行操作,需要用到 QAccessibleTextInterface):
#ifndef ACCESSIBLE_BUTTON_H
#define ACCESSIBLE_BUTTON_H
#include 
#include 
#include 
class AccessibleButton : public QAccessibleWidget
, public QAccessibleTextInterface
{
public:
explicit AccessibleButton(QPushButton *button);
~AccessibleButton();
void *interface_cast(QAccessible::InterfaceType t) Q_DECL_OVERRIDE;
QString text(QAccessible::Text t) const Q_DECL_OVERRIDE;
QString text(int startOffset, int endOffset) const Q_DECL_OVERRIDE;
void selection(int selectionIndex, int *startOffset, int *endOffset) const Q_DECL_OVERRIDE;
int selectionCount() const Q_DECL_OVERRIDE;
void addSelection(int startOffset, int endOffset) Q_DECL_OVERRIDE;
void removeSelection(int selectionIndex) Q_DECL_OVERRIDE;
void setSelection(int selectionIndex, int startOffset, int endOffset) Q_DECL_OVERRIDE;
int cursorPosition() const Q_DECL_OVERRIDE;
void setCursorPosition(int position) Q_DECL_OVERRIDE;
int characterCount() const Q_DECL_OVERRIDE;
QRect characterRect(int offset) const Q_DECL_OVERRIDE;
int offsetAtPoint(const QPoint &point) const Q_DECL_OVERRIDE;
void scrollToSubstring(int startIndex, int endIndex) Q_DECL_OVERRIDE;
QString attributes(int offset, int *startOffset, int *endOffset) const Q_DECL_OVERRIDE;
private:
QPushButton *m_button;
};
#endif // ACCESSIBLE_BUTTON_H
由于访问的是按钮的文本,所以重点关注 interface_cast()、text() 即可,其他接口可以“忽略”:
#include "accessiblebutton.h"
#include 
AccessibleButton::AccessibleButton(QPushButton *button)
: QAccessibleWidget(button)
, m_button(button)
{
}
AccessibleButton::~AccessibleButton()
{
}
void *AccessibleButton::interface_cast(QAccessible::InterfaceType t)
{
switch (t) {
case QAccessible::ActionInterface:
return static_cast(this);
case QAccessible::TextInterface:
return static_cast(this);
default:
return nullptr;
}
}
QString AccessibleButton::text(QAccessible::Text t) const
{
switch (t) {
case QAccessible::Name:
return m_button->accessibleName();
case QAccessible::Description:
return m_button->accessibleDescription();
default:
return QString();
}
}
QString AccessibleButton::text(int startOffset, int endOffset) const
{
Q_UNUSED(startOffset)
Q_UNUSED(endOffset)
return m_button->text();
}
void AccessibleButton::selection(int selectionIndex, int *startOffset, int *endOffset) const
{
Q_UNUSED(selectionIndex)
Q_UNUSED(startOffset)
Q_UNUSED(endOffset)
}
int AccessibleButton::selectionCount() const
{
return 0;
}
void AccessibleButton::addSelection(int startOffset, int endOffset)
{
Q_UNUSED(startOffset)
Q_UNUSED(endOffset)
}
void AccessibleButton::removeSelection(int selectionIndex)
{
Q_UNUSED(selectionIndex)
}
void AccessibleButton::setSelection(int selectionIndex, int startOffset, int endOffset)
{
Q_UNUSED(selectionIndex)
Q_UNUSED(startOffset)
Q_UNUSED(endOffset)
}
int AccessibleButton::cursorPosition() const
{
return 0;
}
void AccessibleButton::setCursorPosition(int position)
{
Q_UNUSED(position)
}
int AccessibleButton::characterCount() const
{
return 0;
}
QRect AccessibleButton::characterRect(int offset) const
{
Q_UNUSED(offset)
return QRect();
}
int AccessibleButton::offsetAtPoint(const QPoint &point) const
{
Q_UNUSED(point)
return 0;
}
void AccessibleButton::scrollToSubstring(int startIndex, int endIndex)
{
Q_UNUSED(startIndex)
Q_UNUSED(endIndex)
}
QString AccessibleButton::attributes(int offset, int *startOffset, int *endOffset) const
{
Q_UNUSED(offset)
Q_UNUSED(startOffset)
Q_UNUSED(endOffset)
return QString();
}
2
实现接口工厂
工厂是一个函数指针,其签名如下:
typedef QAccessibleInterface* myFactoryFunction(const QString &key, QObject *);
该函数接收一个 QString 和一个 QObject 指针,其中 QString 是标识接口的键。QObject 用于传递到 QAccessibleInterface,以便它可以保存对其的引用。
注意:如果 key 和 QObject 没有对应的 QAccessibleInterface,将返回一个空指针。
来看一下我们要实现的工厂示例:
// 接口工厂
QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object)
{
QAccessibleInterface *interface = nullptr;
if (classname == "QPushButton" && object && object->isWidgetType())
interface = new AccessibleButton(static_cast(object));
return interface;
}
3
安装工厂
完成之后,需要使用 QAccessible::installFactory 对上述工厂进行安装:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 安装工厂
QAccessible::installFactory(accessibleFactory);
QPushButton button("test");
QObject::connect(&button, &QPushButton::clicked, [&]() {
static int index = 0;
index++;
button.setText(QString("click %1").arg(index));
});
// 将被辅助技术识别
button.setAccessibleName("button");
button.setAccessibleDescription("this is a simple button");
button.resize(300, 200);
button.show();
return a.exec();
}
4

执行自动化

运行程序,再使用 sniff 工具查看一下,这时已经可以检测出按钮的文本了:

java自动化框架搭建过程 javaui自动化_java 自动化 元素识别_02

然后重新运行下自动化脚本,文本也可以正常打印了:

java自动化框架搭建过程 javaui自动化_#include_03

恭喜,这说明 dogtail 已经可以成功识别 UI 中的元素了。