演练:操作Qt应用中的List

背景

需要针对Qt的QListView组件开发的列表窗口进行操作和自动化测试。QListView通常用于含有大量选项的窗口,比如文件列表、清单等等。以下我们对QListView控件简称List。

目标

本次演练针对List的几个操作进行实现,但实际上CukeTest已经提供了合适的API帮助我们进行自动化了。实现以下几个操作:

  • 选中选项;
  • 滚动列表视图 本次用于测试用的被测应用为Qt中提供的Demo应用——FetchMore,它演示了一个简化的文件浏览工具,可以输入路径来检索路径下的文件/文件夹,界面如下:
    FetchMore应用

为了达成目标,我们需要对列表控件实现以下几个自动化操作:

  1. 输入路径进行筛选
  2. 使用名称或索引定位目标列表项
  3. 点击/选中文件列表项
  4. 滚动列表视图

实际操作

在Qt的列表控件中,存在一些需要特别注意的点。

首先是针对选项的点击、选中操作会自动的滚动到目标选项位置以保证目标选项可见,这是因为桌面应用自身的限制。并且CukeTest为滚动的操作针对List控件提供了scrollTo()方法,针对ListItem控件提供了scrollIntoView()的方法,与Web中的scrollIntoView类似,这些方法能够将视窗滚动到目标位置。

其次是Qt的列表视图为了性能方面的考虑,有时会采取批次加载(Batched Layout)的策略,每次只加载一部分列表,但列表到达底部时继续加载下一批次的列表,这种策略可以有效的避免一次性加载大量列表项带来的卡顿,但我们的自动化也要对其进行相应的处理。CukeTest针对这种策略进行了适配,如果目标选项位置超出了当前列表的范围,那么则会先滚动到底部触发下一批次加载,直到滚动到目标位置。

但是这个策略同样还会带来一些影响,上文提到批次加载的策略会使得只部分加载列表,这就导致针对列表的data()方法可能会获取到不完整,因此还另外提供了scrollToBottom()方法,直接滚动到列表底部直到内容完全加载,此时再调用data()方法便可以取得完整的数据。

项目结构参考

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

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

剧本和模型文件

编辑剧本文件

新建项目后,按照行为驱动测试的最佳实践,首先编写剧本(*.feature文件),编写场景和步骤,然后生成代码模版。步骤在后续的开发中可能会进行调整,但在这一步我们已经通过场景描述对测试脚本的目标有了清晰的了解。

  1. # language: zh-CN
  2. 功能: Qt ListView自动化
  3. 用于QtListView组件的自动化
  4. 用于自动化的应用是FetchMore应用
  5. 场景: 选择目标位置的列表选项
  6. 当搜索路径"/bin"
  7. 那么点击第143个选项
  8. 场景: 选择列表选项
  9. 当搜索路径"/bin"
  10. 那么点击选项"sh"
  11. 场景: 操作列表选项对象
  12. 假如操作对象为列表中的第121个选项
  13. 那么跳转到目标选项位置
  14. 那么点击目标选项

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

剧本文件编辑后的结果如下:
剧本文件

识别控件

首先创建模型文件,接着通过模型管理器打开演练所用的应用——FetchMore。对于本次自动化而言,需要添加的控件很少,只有三个:列表视图自身的List控件、路径输入框Edit控件以及针对第三个场景识别的目标列表选项ListItem

识别完毕后,模型树的内容如下:
模型树内容

执行完上述操作后,模型文件就可以满足对列表和列表项的自动化操作了,当然如果是为了调试或者熟悉应用结构,可以识别添加更多控件。

编写脚本

默认的步骤定义函数文件为definitions1.js,当然文件名不重要,符合文件名规则即可,都可以被项目加载到。

步骤:搜索路径{string}

由于列表操作要需要操作比较长的列表才能完整的演示性能,因此需要先跳转到文件较多的文件夹,这里选择了/bin文件夹,跳转目标文件夹的步骤定义如下:

  1. When("搜索路径{string}", async function (dir) {
  2. await model.getEdit("Edit").set(dir);
  3. });

步骤:点击第{int}个选项

对于点击指定位置选项的步骤,建议使用select()方法,当然click()方法也能够起作用。选中前可以先使用scrollTo()方法滚动到目标位置,使得更好观察结果,下面也会一直使用这种方法来观察运行结果。

  1. Then("点击第{int}个选项", async function (itemIndex) {
  2. let listObject = model.getList("List");
  3. await listObject.scrollTo(itemIndex);
  4. await listObject.select(itemIndex);
  5. });

步骤:点击选项{string}

对于点击指定名称的选项,CukeTest在List控件上提供了一个findItem(itemName)的方法,用于找到列表中指定名称的选项,注意这个方法返回的是一个ListItem类型的自动化对象。因此以下两种写法分别针对ListListItem为目标实现了点击,但是效果是完全一样的。

  1. Then("点击选项{string}", async function (fileName) {
  2. let listObject = model.getList("List");
  3. let targetItem = await listObject.findItem(fileName);
  4. let index = await targetItem.itemIndex();
  5. await listObject.scrollTo(index);
  6. await listObject.select(index);
  7. });

以上代码也可以写作:

  1. Then("点击选项{string}", async function (fileName) {
  2. let listObject = model.getList("List");
  3. let targetItem = await listObject.findItem(fileName);
  4. await targetItem.scrollIntoView();
  5. await targetItem.select();
  6. });

缺省调用scrollTo()方法可以回到顶部。

场景:操作列表选项对象

由于这个场景中的步骤比较短,因此集中在这一节中介绍。这个场景的目的主要是针对List控件下的子控件——ListItem控件的操作,因此我们需要先在模型管理器中识别到这一控件。这里切换到/bin路径下,选择sh选项,将其添加进模型管理器中有如下:

新增的sh选项

  1. Given("操作对象为列表中的第{int}个选项", async function (itemIndex) {
  2. let targetItem = model.getList('List').getItem(itemIndex);
  3. this.targetItem = targetItem;
  4. });
  5. Then("跳转到目标选项位置", async function () {
  6. let targetItem = this.targetItem;
  7. await targetItem.scrollIntoView();
  8. });
  9. Then("点击目标选项", async function () {
  10. let targetItem = this.targetItem;
  11. await targetItem.select();
  12. });

this.targetItem = targetItem代表将变量targetItem的值赋值给场景中的全局对象thistargetItem属性,这个步骤是用于在步骤之间传递对象,在之后还会经常遇到。点击查看World对象具体介绍

其它脚本

hooks.js

按照项目结构介绍,这里创建一个hook.js文件用于编写CukeTest运行钩子。这个钩子里需要实现以下功能:

  • 开始执行步骤前,启动被操作应用,这里是fetch more应用;
  • 每个场景执行完毕后等待几秒方便观察现象;
  • (可选)项目执行完毕后关闭被操作应用,不关闭可以方便观察应用运行结果。

因此代码如下,各个钩子可以从工具箱中直接拖拽生成模板和引用:

  1. const { BeforeAll, After } = require('cucumber');
  2. const { Util } = require('leanpro.common');
  3. const { QtModel, QtAuto } = require('leanpro.qt');
  4. const model = QtModel.loadModel(__dirname + "/../step_definitions/model1.tmodel");
  5. BeforeAll(async function () {
  6. QtAuto.launchQtProcess("/usr/lib/cuketest/bin/fetchmore"); // 前缀为CukeTest默认的sample路径
  7. })
  8. After(async function () {
  9. await Util.delay(2000);
  10. let screenshot = await model.getWindow("Fetch More Example").takeScreenshot();
  11. this.attach(screenshot, 'image/png') // 每个场景结束时截图
  12. })

运行结果

运行结果

总结

以上就是针对Qt中的列表控件的自动化,针对列表的自动化场景不算多,因此举得例子也都比较简单,但是比较全面。在CukeTest提供的操作API加持下,很多无法顺利自动化的桌面应用也可以很成功的实现自动化。