演练3: 操作Qt应用里的TreeView

背景

需要对标准的树状结构——文件系统进行操作,假设需要访问某个固定的路径,使用Qt提供示例应用——DirView为作为自动化的目标应用。

DirView样例界面

目标

针对Qt应用中的树控件,也就是TreeView控件进行自动化。Qt实现树控件的方式更像是一个多层次的表格控件。自动化的目标是能够在DirView应用中,自动的索引到目标路径下。

为了实现该目标,我们需要掌握对树控件的几个操作:

  • 选中树节点
  • 树节点的展开/折叠
  • 搜索树中满足条件的节点(类比列表控件的操作)

在学习这几个操作前,我们先对Qt实现树结构的方法进行一些了解。

Qt中的树控件

下面列举Linux Qt中TreeView控件的几个特点:

  1. 可能延迟加载子节点:对于Qt中有些Tree,例如这里的DirView,会在某个目录节点首次展开时才加载目录里的文件作为子节点。从未被展开起来的子节点可能无法被获取到。因此操作节点最可靠的方式,是按照根节点展开到目标节点路径,来操作目标节点。

  2. Tree结构类似表格:在所有的树控件中,除了各个节点自身外,每个节点都有一些其它的属性,因此对于树控件来说,它不止有多行,而且有多列,类似表格控件。以上述的demo——文件路径视图dirview来说,每行都是一个节点,每个节点都是一个文件/文件夹;而每列都有一个属性名,名称、大小、类型、最后修改时间等属性,这是节点的属性。如下:

DirView的列属性

实际操作

我们先根据目标一步一步掌握基本操作,再通过整合这些操作实现树控件的自动化目标。

项目结构参考

在完成全部的编写后,项目的结构应该呈现如下:

  1. ├── features
  2. ├── feature1.feature
  3. ├── step_definitions
  4. ├── definitions1.js
  5. └── model.tmodel
  6. └── support
  7. └── hooks.js
  8. └── package.json

剧本和模型文件

编辑剧本文件

  1. # language: zh-CN
  2. 功能: QtTree自动化_Linux
  3. 针对Linux Qt中的TreeView控件进行自动化
  4. 场景: 根据itemPath展开树树节点
  5. 假如目标树节点的itemPath"[0,2,8,46,9]",获取该树节点的对象
  6. 那么将目标树节点展开到可视范围内
  7. 那么选中目标树节点并验证
  8. 那么应用截图
  9. 场景: 根据文件路径展开并选中目标树节点
  10. 假如展开"./step_definitions/definitions1.js"文件所在树节点
  11. 那么选中目标树节点并验证
  12. 那么应用截图
  13. 场景: 直接操作目标树节点对象
  14. 假如在模型管理器中添加识别了目标树节点
  15. 那么展开并滚动到目标树节点
  16. 那么展开目标树节点自身
  17. 那么应用截图

编辑模型文件

首先创建模型文件,接着通过模型管理器打开演练所用的应用——DirView。按照之后的目标,我们只需要识别几个目标控件,一个是树状视图自身的Tree控件,接着再识别一个树节点TreeItem控件,这里选取的是一个名为triggers的文件夹。识别完成后如下:
模型文件

编写脚本

场景: 根据itemPath展开树树节点

步骤:目标树节点的itemPath为{string},获取该树节点的对象

由于从步骤描述中接收的itemPath参数为字符串,而我们需要的一个Int[]类型,所以使用JSON.parse()将目标字符串解析为数组,并用于getItem()方法。

  1. let itemPath = JSON.parse(itemPathString);
  2. let targetItem = model.getTree("Dir_View").getItem(itemPath);
  3. if (!targetItem) {
  4. throw "target TreeItem is not exist in this itemPath " + itemPathString;
  5. }
  6. this.item = targetItem; // 利用world对象传递变量
步骤:将目标树节点展开到可视范围内

将目标滚动到可视范围内,首先想到的就是scroll相关的方法,而由于在上一步中已经取得了目标节点的对象,因此我们选择使用scrollIntoView()方法将目标节点移至可视范围。

  1. Then("将目标树节点展开到可视范围内", async function () {
  2. let targetItem = this.item;
  3. await targetItem.scrollIntoView();
  4. });
步骤:选中目标树节点并验证

CukeTest针对TreeItem提供了expand()展开方法,以及获取展开状态的expanded()方法。因此我们可以在选中展开目标节点后,使用断言判断目标节点是否被正确展开:

  1. Then("选中目标树节点并验证", async function () {
  2. let targetItem = this.item;
  3. // 如果目标不在可点击区域内则不会生效
  4. await targetItem.scrollIntoView();
  5. await targetItem.expand();
  6. let isChecked = await targetItem.expanded();
  7. assert.equal(isChecked, true, "没有选中目标树节点");
  8. });

场景: 根据文件路径展开并选中目标树节点

与上面的步骤——目标树节点的itemPath为{string},获取该树节点的对象不同,这里我们希望像普通文件系统一样的去操作DirView应用,因此接受一个路径字符串,展开各层文件夹节点,直到目标文件节点。
这种方式还有另一个优势,在这个步骤中,匹配节点所使用的是节点的名称,相比于使用itemPath属性,节点的位置即使会变化也不会影响结果。

辅助函数genAbsPath()

为了实现这个功能,首先需要一个辅助函数genAbsPath()——用于将路径切分为文件名称组成的数组,方便我们进行节点遍历。

  1. function genAbsPath(relativePath){
  2. let absPath = '';
  3. if(!path.isAbsolute(relativePath)){
  4. absPath = path.join(__dirname, '..', relativePath);
  5. }else{
  6. absPath = relativePath;
  7. }
  8. let pathNodes = absPath.split(path.sep);
  9. pathNodes.shift();
  10. pathNodes.unshift('/');
  11. return pathNodes;
  12. }
展开{string}文件所在树节点
  1. Given("展开{string}文件所在树节点", async function (relativePath) {
  2. let dirNamePath = genAbsPath(relativePath);
  3. let tree = model.getTree('Dir_View');
  4. let targetItem = tree;
  5. for (let i = 0; i < dirNamePath.length; i++) {
  6. targetItem = await targetItem.findItem(dirNamePath[i]);
  7. if(!targetItem){
  8. throw `Can not find the Item named ${dirNamePath[i]}.`
  9. }
  10. await targetItem.expand();
  11. await Util.delay(200);
  12. await targetItem.scrollIntoView();
  13. }
  14. this.item = targetItem; // 在场景中传递TreeItem对象
  15. });

场景: 直接操作目标树节点对象

在这一场景中,我们会使用模型文件中已经识别了的一个树节点,也就是TreeItem自动化对象,并围绕它进行一系列操作来进一步了解树节点的方法。

步骤: 在模型管理器中添加识别了目标树节点

在模型管理器中对应用识别一个节点,为了能够观察到明显的现象,建议选取比较深的节点。这里在模型文件中选取的是/var/lib/dpkg路径下的triggers文件夹,当然你可以选其它的。 因此,这个步骤写作:

  1. Given("在模型管理器中添加识别了目标树节点", async function () {
  2. this.item = model.getTreeItem("triggers");
  3. });
步骤: 展开并滚动到目标树节点

这个步骤或许你有考虑过一层一层的展开直到看见目标树节点。但实际上,TreeItem提供的scrollIntoView()操作方法已经帮你做了这件事。当目标节点不可见时(比如在可视范围外,或者在还未展开的树节点中),scrollIntoView()不仅会滚动,而且会展开所有未展开的父级节点,使得目标树节点可见。
那么脚本就好办了,只需要一行就可以了:

  1. Given("展开到目标树节点", async function () {
  2. let targetItem = this.item;
  3. await targetItem.scrollIntoView();
  4. await Util.delay(500); // 为了方便观察滚动
  5. });
步骤: 展开目标树节点自身

在上一步中,我们将目标树节点展开并移到了可视范围内,但是注意,如果目标树节点本身是可以展开的,那么这个时候节点不会被操作到。因此这一步是展开目标树节点自身,脚本如下:

  1. When("展开目标树节点自身", async function () {
  2. let targetItem = this.item;
  3. let isExpandable = await targetItem.expandable();
  4. if (!isExpandable) {
  5. throw `目标节点无法展开,因为是文件`
  6. } else {
  7. await targetItem.expand();
  8. }
  9. await Util.delay(500);
  10. });

其它脚本

步骤: 应用截图

在场景运行结束后对应用进行截图,以附件的形式贴到运行结果报告中显示,方便观察运行结果。涉及world对象以及附件的知识,可以点击链接深入学习。

  1. Then("应用截图", async function () {
  2. let screenshot = await model.getTree("Dir_View").takeScreenshot();
  3. this.attach(screenshot, 'image/png')
  4. });

将截图当作步骤和像演练:操作Qt应用中的List一样放进hooks.js文件中,两个都可以生效,但是放到生命周期里明显更简洁一点。

编辑hooks.js文件

hooks.js文件存放的通常是CukeTest项目的生命周期(也有直译为钩子)的定义,具体可以了解Hook钩子。熟练的使用hook可以帮助测试人员少写很多重复性的代码,但同样的可能也会损失部分的可读性。
hook.js文件定义如下。这里设置了在项目运行开始前最小化CukeTest完毕后恢复CukeTest,可以避免CukeTest遮挡了运行结果和截图。

  1. const cuketest = require('cuketest');
  2. const { After, AfterAll, BeforeAll, setDefaultTimeout } = require('cucumber');
  3. const { QtAuto } = require("leanpro.qt");
  4. const {Util} = require('leanpro.common');
  5. setDefaultTimeout(30 * 1000); //set step timeout to be 30 seconds
  6. BeforeAll(async function () {
  7. cuketest.minimize();
  8. QtAuto.launchQtProcess("/home/dream/桌面/itemviews/dirview");
  9. })
  10. After(async function () {
  11. await Util.delay(2000);
  12. })
  13. AfterAll(async function () {
  14. cuketest.restore();
  15. cuketest.maximize();
  16. })

运行结果

运行结果

总结

对于Qt树控件有很多嵌套、节点,是一个比较复杂的控件,在自动化的时候,应该要考虑到目标树产生变化的可能性,来写出足够稳定的自动化脚本,因为稳定才是自动化的生命线。通过CukeTest来进一步理解树结构还将进一步提高对树控件自动化的效率,本次演练针对的是比较典型的文件树应用,在实际生产中还会碰到更多运用树控件的应用,只要编写合适的脚本,CukeTest都可以完成对其成功的自动化。