演练: Windows技术操作Qt应用中的QListView
背景
需要针对Qt的ListView组件开发的列表应用进行操作和自动化测试。ListView通常用于含有大量可选项的窗口,比如文件列表、清单等等。以下我们对QListView控件简称为List。
目标
本次自动化的目标是实现对List组件自动化的全面了解,使用CukeTest提供的方法,可以快速的完成自动化。而简单的了解Qt列表的实现方式、以及行为模式,有助于自动化其它表现出列表行为的控件(比如非标准的自绘制列表、下拉框中的选项等等)。
本次演练由浅入深的对List的自动化操作有个全面的认知。
- 直接对列表项
ListItem
的操作; - 掌握列表的滚动操作;
- 掌握列表内容的检索与操作;
本次用于测试用的被测应用为Qt SDK中提供的Demo应用——FetchMore
,它演示了一个简化的文件浏览工具,可以输入路径来检索路径下的文件/文件夹,界面如下:
为了便于管理和理解,以下将不同的操作归类为三个场景:
- 操作目标选项:
- 单击目标项
- 选中目标项
- 滚动列表;
- 使用滚动条按钮进行翻页
- 使用滚动条的方法进行滚动和翻页
- 使用列表方法进行滚动
- 搜索后选中目标
- 在搜索框中输入内容;
- 判断搜索结果中是否存在目标选项;
实际操作
由于Qt应用中,列表中的未显示的选项不会被直接识别到也无法被操作,也就是说,在模型管理器识别了应用中的选项,当它被滚动到不可见区域也会因为被隐藏而无法检测到。因此对于动态变化的选项作为识别对象是不理想的,CukeTest的建议与表格控件Table
、树控件Tree
一样,选择列表(List)这一父控件容器作为识别对象,通过容器上提供的方法操作和获取子控件。
建立项目
编辑剧本文件
新建项目后,按照行为驱动测试的最佳实践,首先编写剧本(*.feature
文件),编写场景和步骤,然后生成代码模版,剧本文件可以切换到文本模式进行修改。
# language: zh-CN
功能: QtList自动化
针对Qt的ListView组件开发的列表窗口进行操作和自动化测试。
场景: 操作目标选项
假如单击目标项"."
假如选中目标项"."
场景: 滚动列表
假如使用列表方法进行滚动
当使用滚动条按钮进行翻页
当使用滚动条的方法进行滚动和翻页
场景: 搜索后选中目标
当在搜索框中输入路径"C:/Program Files"
那么判断搜索结果中是否存在目标项"WindowsPowerShell"
完成剧本文件和代码模版以后如下:
识别控件
由于可访问的ListItem项会随着滚动操作而动态变化,所以我们就没必要识别到具体的ListItem控件,而只要识别到其父控件也就是外部的List控件即可,因此我们可以识别一下列表第一项的.
,因为后面的操作中也会用到。如下红框所示的复选框可以不勾选:
没有勾选的测试对象在点击“添加”按钮时不会添加到模型中。
接着再识别顶部的路径输入框,识别完成
执行完上述操作后,模型文件就可以满足对List
和ListItem
项的简单操作了,然而在后续进一步的操作中偶尔也需要添加新的控件到模型文件中。
编写脚本
单击目标项/选中目标项
单击与选中目标项的区别在于,前者使用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()
调用,默认是左键点击控件正中心。由于点击的是控件中心,而列表项有时候会出现只有不到一半的部分出现在可视范围内(如下图所示),这就有可能会导致点击操作落空,从而导致操作失败。
脚本如下:
Given("单击目标项{string}", async function (itemName) {
let targetItem = await model.getListItem(itemName);
await targetItem.click(1, 1, 1);
await Util.delay(500);
let isFocused = await targetItem.focused();
assert.strictEqual(isFocused, true, `Target item ${itemName} is not selected!`);
});
Given("选中目标项{string}", async function (itemName) {
let targetItem = await model.getListItem(itemName);
await targetItem.select();
await Util.delay(500);
let isFocused = await targetItem.focused();
assert.strictEqual(isFocused, true, `Target item ${itemName} is not selected!`);
});
在点击/选中操作后的延时是考虑到应用的响应时间而加入的,否则获取目标控件选中状态在应用响应之前就完成的话,结果会是未选中。
滚动列表
由于Qt目前不支持使用通用控件方法vScroll()
和hScroll
进行垂直和水平滚动,但我们还可以采用其它的方法可以进行滚动,例如:模拟按键(方向键和PageUp/PageDown键)进行滚动和翻页、使用滚动条按钮进行翻页、使用drag&drop进行拖拽/滑屏操作。但这里我们仅介绍三种适合滚动列表视图的方式:
- 使用滚动条控件滚动
- 使用滚动条控件的方法滚动
- 使用列表控件的方法滚动
这里使用滚动条的按钮滚动的方式考虑的场景————只有一条垂直的滚动条,比较简单。有些时候,还会出现水平的滚动条,这个时候就要区分滚动条进行操作。由于Qt的组件唯一标识符较少,所以通常是在识别时加上
index
属性。正因为可能出现的这种情况,CukeTest推荐的方法还是使用列表控件自身提供的滚动方法进行滚动。更多与滚动操作相关的内容可以点击如何滚动界面查看。
1. 使用滚动条控件滚动
滚动条也是一类可以操作的控件,识别以后甚至能看到它的完整结构,有上下滚动的按钮、有上下翻页的按钮,以及供拖拽的滚动滑块,这里将滚动条中的这五个控件全部侦测添加到模型管理器中,如下图所示:
接着就可以使用click()
方法点击这些控件完成滚动了。
需要注意的是,由于识别时应用中没有水平滚动条,因此仅能识别到唯一的一条滚动条就是垂直滚动条,如果应用后来出现了水平滚动条,则滚动条的控件操作可能会错误的发送到水平滚动条上,下面一个方法也一样。因此在这种情况可能发生的前提下,最好使用列表控件
List
自带的方法进行滚动。JavaScriptWhen("使用滚动条按钮进行翻页", 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'); await lineDown.click(); await Util.delay(1000); await pageDown.click(); await Util.delay(1000); await lineUp.click(); await Util.delay(1000); await pageUp.click(); await Util.delay(1000); });
2. 使用滚动条控件的方法滚动
上文提到,滚动条也是一种控件,叫做ScrollBar
控件,因此它也提供了相当一部分的方法供用户调用,我们就可以通过调用这些方法来控制滚动条,从而完成相应页面的滚动,这里使用的是lineUp()
、lineDown()
、pageUp()
和pageDown()
方法。
When("使用滚动条的方法进行滚动和翻页", async function () {
let scrollbar = model.getScrollBar('ScrollBar');
await scrollbar.lineDown()
await Util.delay(1000);
await scrollbar.lineUp();
await Util.delay(1000);
await scrollbar.pageDown();
await Util.delay(1000);
await scrollbar.pageUp();
await Util.delay(1000);
});
3. 使用列表控件的方法滚动
列表控件List
提供的滚动方法有以下三个: scrollToTop()
、scrollToBottom()
与scrollTo()
方法,分别能够滚动到顶部、滚动到底部以及滚动到指定位置,下面的脚本中演示了三种滚动的调用方式。其中ScrollTo()
方法会滚动到目标项的位置。
Given("使用列表方法进行滚动", async function () {
let targetList = model.getList("List");
let count = await targetList.itemCount();
await targetList.scrollToBottom();
await Util.delay(1000);
await targetList.scrollTo(count);
await Util.delay(1000);
await targetList.scrollToTop();
await Util.delay(1000);
});
搜索后选中目标选项
本次演练中还有两个操作,一个是搜索框的输入,另一个是在搜索结果中检索是否有满足条件的项。
搜索框的输入
应用中的搜索框本质上是一个文本输入框,因此可以使用set()
方法输入指定字符串,这个应用会自动的搜索,如果是需要另外输入ENTER
回车键信号触发搜索的,可以通过在输入值后追加“~”符号来输入回车键,更多特殊按键的信息可以查阅附录:输入键对应表。
When("在搜索框中输入路径{string}", async function (path) {
let searchBox = model.getEdit("Directory:");
await searchBox.click();
await searchBox.set(path);
assert.equal(await searchBox.value(), path);
});
检索结果
前面提到过,由于滚动视窗的原理,只能获取到当前可见的ListItem项,因此检索搜索结果,需要一边滚动一边判断当前页中是否有目标选项,如果要手动编写这样的脚本显得有点儿难度,因此CukeTest为列表控件提供了findItem()
方法,可以在列表中自动的搜索第一个满足条件的列表项ListItem
对象。
由于返回了目标ListItem
对象,因此我们直接调用该对象上的select()
方法就可以完成选中操作了。
Then("判断搜索结果中是否存在目标项{string}", async function (itemName) {
let targetItem = await model.getList('List').findItem(itemName);
await targetItem.select();
await Util.delay(3000);
});
添加Hook
完成了以上脚本的编写,几乎就完成了所有的工作,但是这里为了方便调试,将被测应用的启动和关闭也加入到脚本中,这样就不用手动的去做这些事情了,这也是生命周期(Hook)的工作了。通常来说,这些准备工作也可以写到场景步骤中,但是因为场景中的步骤通常是服务于业务流程与逻辑的,因此加入这些准备工作的脚本可能会有些不合适。
但是从另一个角度来说,如果过分依赖Hook脚本,可能会导致非专业人员的困扰,因为非专业人员主要是通过剧本文件(
.feature
文件)来了解步骤定义,而Hook是不会显示在剧本文件中的,可能会带来阅读上的障碍。
首先,常用的Hook
可以在工具箱的Cucumber
栏目中看到,这里我们拖拽BeforeAll
和AfterAll
这两个Hook
到脚本编辑器中,BeforeAll
会在任何操作运行前执行并且只会执行一次,比如运行项目、运行剧本、运行场景或者是运行步骤前;AfterAll
与BeforeAll
刚好相反,是在所有操作都完成后才会运行,AfterAll
可以用来执行关闭被测应用的操作,这里为了观察操作结果将相关的脚本注释掉了,读者可以反注释掉,只在其中保留了一个恢复CukeTest客户端的脚本。脚本如下:
const { CukeTest } = require('cuketest');
const path = require('path');
let pid = 0;
BeforeAll(async function () {
pid = await Util.launchProcess(path.join(
__dirname,
'..',
'fetchmore.exe'
));
await Util.delay(1000);
CukeTest.minimize(); // CukeTest最小化
})
AfterAll(async function () {
// await Util.stopProcess(pid); // 在调试时可以注释这一行观察结束后的现象
CukeTest.restore(); // CukeTest还原
})
以上就是Qt列表应用的自动化,完整的代码可以前往Github查看CukeTest Demos的Repo。