演练: 操作Qt应用中的Tree

背景

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

DirView样例界面

目标

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

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

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

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

Qt中的树控件

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

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

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

DirView的列属性

实际操作

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

项目结构参考

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

├── features
│   ├── feature1.feature
│   └── step_definitions
│       ├── definitions1.js
│       └── model.tmodel
└── package.json

剧本和模型文件

编辑剧本文件

# language: zh-CN
功能: QtTree自动化_Linux
针对Linux Qt中的TreeView控件进行自动化

  场景: 根据itemPath展开树树节点
    假如目标树节点的itemPath为"[0, 0]",获取该树节点的对象
    那么将目标树节点展开到可视范围内
    那么选中目标树节点并验证
    那么应用截图

  场景: 根据文件路径展开并选中目标树节点
    假如展开到"./step_definitions/definitions1.js"文件所在树节点
    那么选中目标树节点并验证
    那么应用截图

在工具栏中切换文本模式来编辑剧本内容,是一种高级的用法。

剧本编写完毕后,可以点击步骤右侧的灰色箭头,在打开的definitions1.js脚本文件中生成步骤模板

编辑模型文件

首先创建模型文件,接着通过模型管理器打开演练所用的应用——DirView。按照之后的操作,我们只需要识别几个目标控件,一个是树状视图自身的Tree控件,另外再识别一个任意树节点TreeItem控件即可。

模型文件

编写脚本

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

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

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

JavaScript
Given("目标树节点的itemPath为{string},获取该树节点的对象", async function (itemPathString) {
    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; // 步骤间变量传递
});

步骤:将目标树节点展开到可视范围内

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

JavaScript
Then("将目标树节点展开到可视范围内", async function () {
    let targetItem = this.item;
    await targetItem.scrollIntoView();
});

步骤:选中目标树节点并验证

当然CukeTest也提供树节点控件TreeItem的展开expand()操作方法,以及获取展开状态的expanded()属性方法。因此我们可以在选中展开目标节点后,使用断言判断目标节点是否被正确展开:

JavaScript
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应用,只需要传入文件的路径,就能展开各层文件夹节点,直到目标文件节点。而树和树节点控件都提供了findItem()方法,可以在通过节点名称搜索符合条件的节点。
这种方式还有另一个优势,在这个步骤中,匹配节点所使用的是节点的名称,相比于使用itemPath属性,节点的位置即使会变化也不会影响结果。

跨平台支持

为了让这个操作能够在各种操作系统中运行,需要引入一些库来处理系统间的差异,比如Windows的根目录往往是磁盘符(比如C:)而Linux根目录就是/;又比如Windows的文件路径使用反斜杠\分隔,而Linux用斜杠\

解决系统间差异的最好方式是引入path库处理路径,在脚本头部加入path库的引用。

JavaScript
const path = require('path');
接着将路径拆分成一个个文件夹节点:
JavaScript
let relativePath = './'; // 当前路径(相对路径)
let absolutePath = path.resolve(__dirname, '..', relativePath); // 转换成绝对路径
let dirNamePath = absolutePath.split(path.sep); // 切分成数组
console.log(dirNamePath); // 结果为:["C:", "Users", "user", ...]

展开{string}文件所在树节点

由于路径包含多层文件夹,需要一层一层的展开,因此这一步骤可以拆解成以下几步:

  1. 在当前层级中搜索目标节点;
  2. 搜索到后展开该节点,进入下一层级;
  3. 重复1-2操作直到搜索不到目标节点后结束。
    JavaScript
    Given("展开到{string}文件所在树节点", async function (relativePath) {
    
        // 将路径拆分成路径节点数组,结果类似["C:", "Users", "user", ...]
        let dirNamePath = path.resolve(__dirname, '..', relativePath).split(path.sep);
        this.attach(`pathNodes: [${dirNamePath}]`);
        let tree = model.getTree('Dir_View');
        let targetItem;
    
        // 由于路径根节点需要特殊处理
        // Windows系统中为磁盘名+磁盘符
        // Linux系统中为`/`,在pathNodes中表现为空字符''
        let root = dirNamePath[0];
        if (root === '') {
            targetItem = await tree.findItem('/');
        } else {
            // 根节点为磁盘符时定位根节点
            let rootNodeList = await tree.children();
            await Promise.all(rootNodeList.map(async node => {
                let nodeName = await node.value();
                if (nodeName.indexOf(root) !== -1) {
                    targetItem = node;
                }
            }));
        }
        await targetItem.expand();
        await Util.delay(200);
        await targetItem.scrollIntoView();
    
        // 处理完根节点应该继续从第二个节点展开
        for (let i = 1; 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对象
    });

其它脚本

步骤: 应用截图

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

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

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

引入Hooks

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

JavaScript
const { CukeTest } = require('cuketest');
const { After, AfterAll, BeforeAll, setDefaultTimeout } = require('cucumber');
const {Util} = require('leanpro.common');
/// 超时时间和Hook设置 ///
setDefaultTimeout(30 * 1000);  //set step timeout to be 30 seconds
let proc;
BeforeAll(async function () {
    CukeTest.minimize();
    let extension = (process.platform === 'win32') ? '.exe' : '';
    proc = await QtAuto.launchQtProcessAsync(path.dirname(process.execPath) + "/bin/dirview" + extension);
    await model.getApplication("dirview").exists(10);
})

After(async function () {
    let screenshot = await model.getTree("Dir_View").takeScreenshot();
    this.attach(screenshot, 'image/png');
    await Util.delay(2000);
})

AfterAll(async function () {
    CukeTest.restore();
    CukeTest.maximize();
    Util.stopProcess(proc)
})

运行结果

运行结果

总结

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

results matching ""

    No results matching ""