演练:创建树控件遍历自动化项目

本次的演练基于Windows系统的资源管理器作为被测应用,资源管理器右侧导航栏中的树是Windows原生UI框架实现树状结构的好例子,本次演练的目的就是遍历这棵树。

完整的用例源码可以从示例页面打开文件浏览器遍历示例项目来获得。

编辑剧本

本次操作主要集中在遍历树这一操作上,因此剧本上不需要做太多处理:

# language: zh-CN
功能: 遍历原生Windows应用树节点
以Windows资源管理器为被测应用进行树节点的遍历

  场景: 遍历资源管理器
    假如打开资源管理器
    当遍历展开树"树视图"
    那么将结果附件

侦测控件

使用导航方法检索控件的一个优势就在于只需要侦测添加很少的控件对象(大多数情况下只需要添加一个控件对象作为导航的起点/锚点),而在本次操作的资源管理器树中,所有的树节点(也就是TreeItem控件)都位于同一个视图容器(即Tree控件)下,所以将这个容器作为锚点,它的第一个子节点就是整棵树的第一个树节点,将其作为起点来遍历整棵树无疑是最合适的。

首先打开资源管理器,侦测右侧导航栏中的任意位置,在弹出的侦测窗口中取消勾选除了Tree控件以外的其它控件。
侦测目标

侦测结果

红框中的识别属性也需要取消勾选,原因如下:
左边红框:窗口标题,资源管理器的窗口标题会跟随当前所在的目录改变,是一个动态属性,不应该作为识别属性;
右边红框Tree控件名称,由于Win7与Win10上对该控件的名称不同,为了模型能在两个版本上通用,不将其作为识别属性。

最后得到的模型树应该如下,只有两个控件:
模型树

编写脚本

下面讲讲脚本的编写思路,由于目的是遍历树,所以采用的递归的写法;但是文件树的层次比较多,因此另外传递了深度depth到递归函数中,并通过设置最大深度MAX_DEPTH的值来控制遍历深度。

步骤1: 打开资源管理器

直接使用Util.launchProcess()方法启动资源管理器,可以在任务管理器中查看资源管理器的可执行文件路径,也可以省略路径直接使用可执行文件名称"explorer",大部分系统内置程序都可以像这样省略路径直接启动,比如计算器就是"calc"。所以步骤定义脚本如下:

JavaScript
Given("打开资源管理器", async function () {
    Util.launchProcess("explorer");
    if (!await model.getWindow("Window").exists(5))
    {
        throw Error("资源管理器没有正常启动")
    }
});

步骤2: 遍历展开树{string}

这一步是主要的步骤,是以目标Tree控件为锚点,遍历一定深度的所有树节点TreeItem,因此是可迁移的,不止适用于资源管理器的导航树一个场景。步骤定义脚本如下:

JavaScript
const MAX_DEPTH = 3; // 遍历的最大深度,如3就代表最多展开三级节点
const result = []; // 记录遍历节点的名称和深度信息用于生成记录,成员为{name, depth}对象

When("遍历展开树{string}", async function (tree) {
    let depth = 0
    let RootNode = await model.getTree(tree).firstChild("TreeItem"); // 获取树中的第一个`TreeItem`子节点
    if(RootNode){
        await expandChild(RootNode, depth);
    }else{
        throw Error(`当前${tree}树中没有任何树节点。`)
    }
});
接着是重头戏,我们需要实现递归遍历的函数expandChild(node, depth),如下:
JavaScript
async function expandChild(node, depth) { //_0_
    await node.expand();  // _1_
    let nodeName = await node.name();
    result.push({name:nodeName, depth: depth}); // _2_
    await Util.delay(100); // _3_

    const childNode = await node.firstChild("TreeItem"); // _4_
    if (childNode && depth + 1 < MAX_DEPTH) {  // _5_
        await expandChild(childNode, depth + 1);
    }else{
        // _6_
    }
    
    const nextNode = await node.next(); // _7_
    if (nextNode) {
        await expandChild(nextNode, depth);
    }else{
        return;
    }
}

下面一步步介绍脚本实现的逻辑:

  1. 递归函数,需要传递控件对象和深度信息,如果到达最大深度会停止并上浮。
  2. 展开控件对象,CukeTest的操作逻辑不区分目标节点处在展开或者折叠状态,并且不管目标节点是否可展开都会尝试展开。
  3. 将当前节点的信息添加到先前定义的全局变量result中,用于之后生成记录
  4. 添加等待动画效果的延时,由于很多树的展开/折叠会添加动画效果,因此这个延时就用于等待动画完成。
  5. 遍历采用的是深度优先的策略,因此只要当前节点存在子节点就会递归深入到子节点。
  6. 短路原则,只要超出最大遍历深度,无论有无子节点都不会继续递归深入。
  7. 当没有子节点时或最深节点的操作,注意与叶子节点区分。
  8. 如果子节点已经全部遍历,则继续遍历下一个兄弟节点,当没有下一个兄弟节点时,即到达当前层次底部,返回上一层。

步骤3: 将结果附件

最后将遍历过的节点记录作为附件添加到运行报告中:

JavaScript
Then("将结果附件", async function () {
    let report = "";
    for(let row of result){
        let rowString = '\t'.repeat(row.depth)+row.name+'\n';
        report += rowString;
    }
    this.attach(report);
});

简单的将result数组中的每个成员作为一行字符串,并根据深度添加缩进即可。

results matching ""

    No results matching ""