演练:操作Qt应用中的List

背景

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

目标

本次自动化的目标是实现对List组件自动化的全面了解,使用CukeTest提供的方法,可以快速的完成自动化。而简单的了解Qt列表的实现方式、以及行为模式,有助于自动化其它表现出列表行为的控件(比如非标准的自绘制列表、下拉框中的选项等等)。

本次演练由浅入深的对List的自动化操作有个全面的认知。

  • 直接对列表项ListItem的操作;
  • 掌握列表的滚动操作;
  • 掌握列表内容的检索与操作;

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

为了便于管理和理解,以下将不同的操作归类为三个场景:

  1. 操作目标选项:
    1. 单击目标项
    2. 选中目标项
  2. 滚动列表;
    1. 使用滚动条按钮进行翻页
    2. 使用滚动条的方法进行滚动和翻页
    3. 使用列表方法进行滚动
  3. 搜索后选中目标
    1. 在搜索框中输入内容;
    2. 判断搜索结果中是否存在目标选项;

实际操作

由于Qt应用中,列表中的未显示的选项不会被直接识别到也无法被操作,也就是说,在模型管理器识别了应用中的选项,当它被滚动到不可见区域也会因为被隐藏而无法检测到。因此对于动态变化的选项作为识别对象是不理想的,CukeTest的建议与表格控件Table、树控件Tree一样,选择列表(List)这一父控件容器作为识别对象,通过容器上提供的方法操作和获取子控件。

建立项目

编辑剧本文件

新建项目后,按照行为驱动测试的最佳实践,首先编写剧本(*.feature文件),编写场景和步骤,然后生成代码模版,剧本文件可以切换到文本模式进行修改。

  1. # language: zh-CN
  2. 功能: QtList自动化
  3. 针对QtListView组件开发的列表窗口进行操作和自动化测试。
  4. 场景: 操作目标选项
  5. 假如单击目标项"."
  6. 假如选中目标项"."
  7. 场景: 滚动列表
  8. 假如使用列表方法进行滚动
  9. 当使用滚动条按钮进行翻页
  10. 当使用滚动条的方法进行滚动和翻页
  11. 场景: 搜索后选中目标
  12. 当在搜索框中输入路径"C:/Program Files"
  13. 那么判断搜索结果中是否存在目标项"WindowsPowerShell"

完成剧本文件和代码模版以后如下:

QtList自动化剧本文件

识别控件

由于可访问的ListItem项会随着滚动操作而动态变化,所以我们就没必要识别到具体的ListItem控件,而只要识别到其父控件也就是外部的List控件即可,因此我们可以识别一下列表第一项的.,因为后面的操作中也会用到。如下红框所示的复选框可以不勾选:

模型文件

没有勾选的测试对象在点击“添加”按钮时不会添加到模型中。

接着再识别顶部的路径输入框,识别完成 执行完上述操作后,模型文件就可以满足对ListListItem项的简单操作了,然而在后续进一步的操作中偶尔也需要添加新的控件到模型文件中。

编写脚本

单击目标项/选中目标项

单击与选中目标项的区别在于,前者使用click()也就是控件的鼠标点击方法,对于列表选项来说也可以选中,但存在隐患,具体是什么隐患我们后面再说;而选中目标项使用的是列表选项提供的select()方法,相对来说更可靠。

因为需要单击目标选项,我们复习一下click()方法的调用方法,click()方法可以传入三个参数,分别是点击的相对横坐标x、纵坐标y,以及点击的鼠标按钮: 1为左键,2为右键。所以我们调用点击控件,可以直接调用click(0,0,1)。而如果需要右键,只需要将click(0,0,1)改为click(0,0,2)即可。但这就结束了吗,不,我们上文中提到这种选中对列表项来说存在隐患,是因为CukeTest中调用click(0,0,1)等同于缺省调用点击方法,即不传任何参数的click()调用,默认是左键点击控件正中心。由于点击的是控件中心,而列表项有时候会出现只有不到一半的部分出现在可视范围内(如下图所示),这就有可能会导致点击操作落空,从而导致操作失败。 部分超出屏幕范围

脚本如下:

  1. Given("单击目标项{string}", async function (itemName) {
  2. let targetItem = await model.getListItem(itemName);
  3. await targetItem.click(1, 1, 1);
  4. await Util.delay(500);
  5. let isFocused = await targetItem.focused();
  6. assert.strictEqual(isFocused, true, `Target item ${itemName} is not selected!`);
  7. });
  8. Given("选中目标项{string}", async function (itemName) {
  9. let targetItem = await model.getListItem(itemName);
  10. await targetItem.select();
  11. await Util.delay(500);
  12. let isFocused = await targetItem.focused();
  13. assert.strictEqual(isFocused, true, `Target item ${itemName} is not selected!`);
  14. });

在点击/选中操作后的延时是考虑到应用的响应时间而加入的,否则获取目标控件选中状态在应用响应之前就完成的话,结果会是未选中

滚动列表

由于Qt目前不支持使用通用控件方法vScroll()hScroll进行垂直和水平滚动,但我们还可以采用其它的方法可以进行滚动,例如:模拟按键(方向键和PageUp/PageDown键)进行滚动和翻页、使用滚动条按钮进行翻页、使用drag&drop进行拖拽/滑屏操作。但这里我们仅介绍三种适合滚动列表视图的方式:

  1. 使用滚动条控件滚动
  2. 使用滚动条控件的方法滚动
  3. 使用列表控件的方法滚动

这里使用滚动条的按钮滚动的方式考虑的场景————只有一条垂直的滚动条,比较简单。有些时候,还会出现水平的滚动条,这个时候就要区分滚动条进行操作。由于Qt的组件唯一标识符较少,所以通常是在识别时加上index属性。正因为可能出现的这种情况,CukeTest推荐的方法还是使用列表控件自身提供的滚动方法进行滚动。更多与滚动操作相关的内容可以点击如何滚动界面查看。

1. 使用滚动条控件滚动

滚动条也是一类可以操作的控件,识别以后甚至能看到它的完整结构,有上下滚动的按钮、有上下翻页的按钮,以及供拖拽的滚动滑块,这里将滚动条中的这五个控件全部侦测添加到模型管理器中,如下图所示:
列表及滚动条模型

接着就可以使用click()方法点击这些控件完成滚动了。

需要注意的是,由于识别时应用中没有水平滚动条,因此仅能识别到唯一的一条滚动条就是垂直滚动条,如果应用后来出现了水平滚动条,则滚动条的控件操作可能会错误的发送到水平滚动条上,下面一个方法也一样。因此在这种情况可能发生的前提下,最好使用列表控件List自带的方法进行滚动。 ```js When(“使用滚动条按钮进行翻页”, async function () { // 在模型文件中添加滚动条的测试对象 let lineUp = model.getButton(“Line up”); let lineDown = model.getButton(‘Line down’) let pageUp = model.getButton(‘Page up’); let pageDown = model.getButton(‘Page down’);

  1. await lineDown.click();
  2. await Util.delay(1000);
  3. await pageDown.click();
  4. await Util.delay(1000);
  5. await lineUp.click();
  6. await Util.delay(1000);
  7. await pageUp.click();
  8. await Util.delay(1000);

});

  1. ##### 2. 使用滚动条控件的方法滚动
  2. 上文提到,滚动条也是一种控件,叫做`ScrollBar`控件,因此它也提供了相当一部分的方法供用户调用,我们就可以通过调用这些方法来控制滚动条,从而完成相应页面的滚动,这里使用的是`lineUp()``lineDown()``pageUp()``pageDown()`方法。
  3. ```js
  4. When("使用滚动条的方法进行滚动和翻页", async function () {
  5. let scrollbar = model.getScrollBar('ScrollBar');
  6. await scrollbar.lineDown()
  7. await Util.delay(1000);
  8. await scrollbar.lineUp();
  9. await Util.delay(1000);
  10. await scrollbar.pageDown();
  11. await Util.delay(1000);
  12. await scrollbar.pageUp();
  13. await Util.delay(1000);
  14. });
3. 使用列表控件的方法滚动

列表控件List提供的滚动方法有以下三个: scrollToTop()scrollToBottom()scrollTo()方法,分别能够滚动到顶部、滚动到底部以及滚动到指定位置,下面的脚本中演示了三种滚动的调用方式。其中ScrollTo()方法会滚动到目标项的位置。

  1. Given("使用列表方法进行滚动", async function () {
  2. let targetList = model.getList("List");
  3. let count = await targetList.itemCount();
  4. await targetList.scrollToBottom();
  5. await Util.delay(1000);
  6. await targetList.scrollTo(count);
  7. await Util.delay(1000);
  8. await targetList.scrollToTop();
  9. await Util.delay(1000);
  10. });

搜索后选中目标选项

本次演练中还有两个操作,一个是搜索框的输入,另一个是在搜索结果中检索是否有满足条件的项。

搜索框的输入

应用中的搜索框本质上是一个文本输入框,因此可以使用set()方法输入指定字符串,这个应用会自动的搜索,如果是需要另外输入ENTER回车键信号触发搜索的,可以通过在输入值后追加“~”符号来输入回车键,更多特殊按键的信息可以查阅附录:输入键对应表

  1. When("在搜索框中输入路径{string}", async function (path) {
  2. let searchBox = model.getEdit("Directory:");
  3. await searchBox.click();
  4. await searchBox.set(path);
  5. assert.equal(await searchBox.value(), path);
  6. });
检索结果

前面提到过,由于滚动视窗的原理,只能获取到当前可见的ListItem项,因此检索搜索结果,需要一边滚动一边判断当前页中是否有目标选项,如果要手动编写这样的脚本显得有点儿难度,因此CukeTest为列表控件提供了findItem()方法,可以在列表中自动的搜索第一个满足条件的列表项ListItem对象,当然findItem()方法还有更高级的用法,这里就先不介绍了。

由于返回了目标ListItem对象,因此我们直接调用该对象上的select()方法就可以完成选中操作了。

  1. Then("判断搜索结果中是否存在目标项{string}", async function (itemName) {
  2. let targetItem = await model.getList('List').findItem(itemName);
  3. await targetItem.select();
  4. await Util.delay(3000);
  5. });

添加Hook

完成了以上脚本的编写,几乎就完成了所有的工作,但是这里为了方便调试,将被测应用的启动和关闭也加入到脚本中,这样就不用手动的去做这些事情了,这也是生命周期(Hook)的工作了。通常来说,这些准备工作也可以写到场景步骤中,但是因为场景中的步骤通常是服务于业务流程与逻辑的,因此加入这些准备工作的脚本可能会有些不合适。

但是从另一个角度来说,如果过分依赖Hook脚本,可能会导致非专业人员的困扰,因为非专业人员主要是通过剧本文件(.feature文件)来了解步骤定义,而Hook是不会显示在剧本文件中的,可能会带来阅读上的障碍。

首先,常用的Hook可以在工具箱的Cucumber栏目中看到,这里我们拖拽BeforeAllAfterAll这两个Hook到脚本编辑器中,BeforeAll会在任何操作运行前执行并且只会执行一次,比如运行项目、运行剧本、运行场景或者是运行步骤前;AfterAllBeforeAll刚好相反,是在所有操作都完成后才会运行,AfterAll可以用来执行关闭被测应用的操作,这里为了观察操作结果将相关的脚本注释掉了,读者可以反注释掉,只在其中保留了一个恢复CukeTest客户端的脚本。脚本如下:

  1. const cuketest = require('cuketest');
  2. const path = require('path');
  3. let pid = 0;
  4. BeforeAll(async function () {
  5. pid = await Util.launchProcess(path.join(
  6. __dirname,
  7. '..',
  8. 'fetchmore.exe'
  9. ));
  10. await Util.delay(1000);
  11. cuketest.minimize(); // CukeTest最小化
  12. })
  13. AfterAll(async function () {
  14. // await Util.stopProcess(pid); // 在调试时可以注释这一行观察结束后的现象
  15. cuketest.restore(); // CukeTest还原
  16. })

以上就是Qt列表应用的自动化,完整的代码可以前往Github查看CukeTest Demos的Repo。