演练3: 操作Qt应用里的TreeView
背景
需要对标准的树状结构——文件系统进行操作,假设需要访问某个固定的路径,使用Qt提供示例应用——DirView为作为自动化的目标应用。
目标
针对Qt应用中的树控件,也就是TreeView
控件进行自动化。Qt实现树控件的方式更像是一个多层次的表格控件。自动化的目标是能够在DirView应用中,自动的索引到目标路径下。
为了实现该目标,我们需要掌握对树控件的几个操作:
- 选中树节点
- 树节点的展开/折叠
- 搜索树中满足条件的节点(类比列表控件的操作)
在学习这几个操作前,我们先对Qt实现树结构的方法进行一些了解。
Qt中的树控件
下面列举Linux Qt中TreeView控件的几个特点:
可能延迟加载子节点:对于Qt中有些Tree,例如这里的DirView,会在某个目录节点首次展开时才加载目录里的文件作为子节点。从未被展开起来的子节点可能无法被获取到。因此操作节点最可靠的方式,是按照根节点展开到目标节点路径,来操作目标节点。
Tree结构类似表格:在所有的树控件中,除了各个节点自身外,每个节点都有一些其它的属性,因此对于树控件来说,它不止有多行,而且有多列,类似表格控件。以上述的demo——文件路径视图
dirview
来说,每行都是一个节点,每个节点都是一个文件/文件夹;而每列都有一个属性名,名称、大小、类型、最后修改时间等属性,这是节点的属性。如下:
实际操作
我们先根据目标一步一步掌握基本操作,再通过整合这些操作实现树控件的自动化目标。
项目结构参考
在完成全部的编写后,项目的结构应该呈现如下:
├── features
│ ├── feature1.feature
│ ├── step_definitions
│ │ ├── definitions1.js
│ │ └── model.tmodel
│ └── support
│ └── hooks.js
└── package.json
剧本和模型文件
编辑剧本文件
# language: zh-CN
功能: QtTree自动化_Linux
针对Linux Qt中的TreeView控件进行自动化
场景: 根据itemPath展开树树节点
假如目标树节点的itemPath为"[0,2,8,46,9]",获取该树节点的对象
那么将目标树节点展开到可视范围内
那么选中目标树节点并验证
那么应用截图
场景: 根据文件路径展开并选中目标树节点
假如展开"./step_definitions/definitions1.js"文件所在树节点
那么选中目标树节点并验证
那么应用截图
场景: 直接操作目标树节点对象
假如在模型管理器中添加识别了目标树节点
那么展开并滚动到目标树节点
那么展开目标树节点自身
那么应用截图
编辑模型文件
首先创建模型文件,接着通过模型管理器打开演练所用的应用——DirView
。按照之后的目标,我们只需要识别几个目标控件,一个是树状视图自身的Tree
控件,接着再识别一个树节点TreeItem
控件,这里选取的是一个名为triggers
的文件夹。识别完成后如下:
编写脚本
场景: 根据itemPath展开树树节点
步骤:目标树节点的itemPath为{string},获取该树节点的对象
由于从步骤描述中接收的itemPath
参数为字符串,而我们需要的一个Int[]
类型,所以使用JSON.parse()
将目标字符串解析为数组,并用于getItem()
方法。
let itemPath = JSON.parse(itemPathString);
let targetItem = model.getTree("Dir_View").getItem(itemPath);
if (!targetItem) {
throw "target TreeItem is not exist in this itemPath " + itemPathString;
}
this.item = targetItem; // 利用world对象传递变量
步骤:将目标树节点展开到可视范围内
将目标滚动到可视范围内,首先想到的就是scroll
相关的方法,而由于在上一步中已经取得了目标节点的对象,因此我们选择使用scrollIntoView()
方法将目标节点移至可视范围。
Then("将目标树节点展开到可视范围内", async function () {
let targetItem = this.item;
await targetItem.scrollIntoView();
});
步骤:选中目标树节点并验证
CukeTest针对TreeItem
提供了expand()
展开方法,以及获取展开状态的expanded()
方法。因此我们可以在选中展开目标节点后,使用断言判断目标节点是否被正确展开:
Then("选中目标树节点并验证", async function () {
let targetItem = this.item;
// 如果目标不在可点击区域内则不会生效
await targetItem.scrollIntoView();
await targetItem.expand();
let isChecked = await targetItem.expanded();
assert.equal(isChecked, true, "没有选中目标树节点");
});
场景: 根据文件路径展开并选中目标树节点
与上面的步骤——目标树节点的itemPath为{string},获取该树节点的对象
不同,这里我们希望像普通文件系统一样的去操作DirView应用,因此接受一个路径字符串,展开各层文件夹节点,直到目标文件节点。
这种方式还有另一个优势,在这个步骤中,匹配节点所使用的是节点的名称,相比于使用itemPath
属性,节点的位置即使会变化也不会影响结果。
辅助函数genAbsPath()
为了实现这个功能,首先需要一个辅助函数genAbsPath()
——用于将路径切分为文件名称组成的数组,方便我们进行节点遍历。
function genAbsPath(relativePath){
let absPath = '';
if(!path.isAbsolute(relativePath)){
absPath = path.join(__dirname, '..', relativePath);
}else{
absPath = relativePath;
}
let pathNodes = absPath.split(path.sep);
pathNodes.shift();
pathNodes.unshift('/');
return pathNodes;
}
展开{string}文件所在树节点
Given("展开{string}文件所在树节点", async function (relativePath) {
let dirNamePath = genAbsPath(relativePath);
let tree = model.getTree('Dir_View');
let targetItem = tree;
for (let i = 0; i < dirNamePath.length; i++) {
targetItem = await targetItem.findItem(dirNamePath[i]);
if(!targetItem){
throw `Can not find the Item named ${dirNamePath[i]}.`
}
await targetItem.expand();
await Util.delay(200);
await targetItem.scrollIntoView();
}
this.item = targetItem; // 在场景中传递TreeItem对象
});
场景: 直接操作目标树节点对象
在这一场景中,我们会使用模型文件中已经识别了的一个树节点,也就是TreeItem
自动化对象,并围绕它进行一系列操作来进一步了解树节点的方法。
步骤: 在模型管理器中添加识别了目标树节点
在模型管理器中对应用识别一个节点,为了能够观察到明显的现象,建议选取比较深的节点。这里在模型文件中选取的是/var/lib/dpkg
路径下的triggers
文件夹,当然你可以选其它的。 因此,这个步骤写作:
Given("在模型管理器中添加识别了目标树节点", async function () {
this.item = model.getTreeItem("triggers");
});
步骤: 展开并滚动到目标树节点
这个步骤或许你有考虑过一层一层的展开直到看见目标树节点。但实际上,TreeItem
提供的scrollIntoView()
操作方法已经帮你做了这件事。当目标节点不可见时(比如在可视范围外,或者在还未展开的树节点中),scrollIntoView()
不仅会滚动,而且会展开所有未展开的父级节点,使得目标树节点可见。
那么脚本就好办了,只需要一行就可以了:
Given("展开到目标树节点", async function () {
let targetItem = this.item;
await targetItem.scrollIntoView();
await Util.delay(500); // 为了方便观察滚动
});
步骤: 展开目标树节点自身
在上一步中,我们将目标树节点展开并移到了可视范围内,但是注意,如果目标树节点本身是可以展开的,那么这个时候节点不会被操作到。因此这一步是展开目标树节点自身,脚本如下:
When("展开目标树节点自身", async function () {
let targetItem = this.item;
let isExpandable = await targetItem.expandable();
if (!isExpandable) {
throw `目标节点无法展开,因为是文件`
} else {
await targetItem.expand();
}
await Util.delay(500);
});
其它脚本
步骤: 应用截图
在场景运行结束后对应用进行截图,以附件的形式贴到运行结果报告中显示,方便观察运行结果。涉及world对象以及附件的知识,可以点击链接深入学习。
Then("应用截图", async function () {
let screenshot = await model.getTree("Dir_View").takeScreenshot();
this.attach(screenshot, 'image/png')
});
将截图当作步骤和像演练:操作Qt应用中的List一样放进
hooks.js
文件中,两个都可以生效,但是放到生命周期里明显更简洁一点。
编辑hooks.js文件
hooks.js
文件存放的通常是CukeTest项目的生命周期(也有直译为钩子)的定义,具体可以了解Hook钩子。熟练的使用hook可以帮助测试人员少写很多重复性的代码,但同样的可能也会损失部分的可读性。hook.js
文件定义如下。这里设置了在项目运行开始前最小化CukeTest完毕后恢复CukeTest,可以避免CukeTest遮挡了运行结果和截图。
const cuketest = require('cuketest');
const { After, AfterAll, BeforeAll, setDefaultTimeout } = require('cucumber');
const { QtAuto } = require("leanpro.qt");
const {Util} = require('leanpro.common');
setDefaultTimeout(30 * 1000); //set step timeout to be 30 seconds
BeforeAll(async function () {
cuketest.minimize();
QtAuto.launchQtProcess("/home/dream/桌面/itemviews/dirview");
})
After(async function () {
await Util.delay(2000);
})
AfterAll(async function () {
cuketest.restore();
cuketest.maximize();
})
运行结果
总结
对于Qt树控件有很多嵌套、节点,是一个比较复杂的控件,在自动化的时候,应该要考虑到目标树产生变化的可能性,来写出足够稳定的自动化脚本,因为稳定才是自动化的生命线。通过CukeTest来进一步理解树结构还将进一步提高对树控件自动化的效率,本次演练针对的是比较典型的文件树应用,在实际生产中还会碰到更多运用树控件的应用,只要编写合适的脚本,CukeTest都可以完成对其成功的自动化。