演練:操作 Qt 應用中的 QListView
Qt 應用中的清單控件——QListView,這篇文章主要介紹了如何通過CukeTest全面自動化該控件。
背景
需要針對 Qt 的 QListView 元件開發的清單視窗進行操作和自動化測試。QListView 通常用于含有大量可選項的視窗,比如檔案清單、清單等等。以下我們對 QListView 控件簡稱 List。
目标
為了實作對 List 元件自動化的全面了解,本次演練由淺入深的對 List 的自動化操作有個全面的認知。
- 完成對清單(List)的操作:滾動、滑動、檢索内容;
- 完成對選項(ListItem)的操作:單擊、輕按兩下,右鍵操作;
本次用于測試用的被測應用為 Qt SDK 中提供的 Demo 應用——FetchMore,它示範了一個簡化的檔案浏覽工具,可以輸入路徑來檢索路徑下的檔案/檔案夾,界面如下:
為了便于管理和了解,以下将不同的操作歸類為三個場景:
- 操作目标選項(無需滾動):
- 單擊目标項
- 輕按兩下目标項
- 右鍵目标項并選擇操作
- 滾動清單;
- 使用模拟按鍵進行滾動和翻頁;
- 使用滾動條按鈕進行翻頁;
- 使用 drag&drop 進行拖拽/滑屏操作;
- 使用 vScroll 和 hScroll 進行滾動(Qt 暫不支援);
- 搜尋後選中目标
- 在搜尋框中輸入内容;
- 判斷搜尋結果中是否存在目标選項;
實際操作
由于 Qt 應用中,清單中的未顯示的選項不會被直接識别到,也就是說,使用模型管理器對應用中的選項進行識别時,僅能識别到目前頁中的可見選項,原來識别到的選項一旦滾動到不可見區域就會因為被隐藏而無法檢測到。是以對于動态變化的選項作為識别對象是不理想的。這裡選擇了與演練:操作 Qt 應用中的表格[1]中類似的方法,選擇清單(List)作為識别對象,然後針對這個容器,通過擷取子控件的方式取得子控件。
建立項目
編輯劇本檔案
建立項目後,按照行為驅動測試的最佳實踐,首先編寫劇本(*.feature 檔案),編寫場景和步驟,然後生成代碼模版。步驟在後續的開發中可能會進行調整,但在這一步我們已經通過場景描述對測試腳本的目标有了清晰的了解。
# language: zh-CN
功能: QtList自動化
針對Qt的ListView元件開發的清單視窗進行操作和自動化測試。
場景: 操作目标選項
假如單擊目标項
當輕按兩下目标項
那麼右鍵目标項并選擇操作
場景: 滾動清單
假如使用模拟按鍵進行滾動和翻頁
當使用滾動條按鈕進行翻頁
那麼使用drag&drop進行拖拽/滑屏操作
那麼使用vScroll和hScroll進行滾動(Qt暫不支援)
場景: 搜尋後選中目标
當在搜尋框中輸入内容
那麼判斷搜尋結果中是否存在目标項
完成劇本檔案和代碼模版以後如下:
QtList自動化劇本檔案
識别控件
由于可通路的 ListItem 項會随着滾動操作而動态變化,是以我們就沒必要識别到具體的 ListItem 控件,而隻要識别到其父控件也就是外部的 List 控件即可,如下紅框所示的複選框可以不勾選:
模型檔案
沒有勾選的測試對象[2]在點選“添加”按鈕時不會添加到模型中。
執行完上述操作後,模型檔案就可以滿足對 List 和 ListItem 項的簡單操作了,然而在後續進一步的操作中偶爾也需要添加新的控件到模型檔案中。
編寫腳本
操作目标選項
因為需要對左擊、輕按兩下以及右擊目标選項,左擊是
click()
方法,輕按兩下是
dblClick()
方法,那麼如何右擊呢?
click()
方法可以傳入三個參數,分别是點選的相對橫坐标、縱坐标,以及點選的按鈕,
1
為左鍵,
2
為右鍵,是以隻需要将
click(0,0,1)
改為
click(0,0,2)
即可右擊目标元件。是以有代碼如下:
Given("單擊目标項{string}", async function (itemName) {
let targetItem = await model.getListItem(itemName);
await targetItem.click(0, 0, 1);
});
When("輕按兩下目标項{string}", async function (itemName) {
let targetItem = await model.getListItem(itemName);
await targetItem.dblClick(0, 0, 1);
await Util.delay(100);
// 判斷目标項是否被選中
let isFocused = await targetItem.focused();
await Util.delay(1000);
assert.equal(isFocused, true, "Target item is not selected!");
});
Then("右鍵目标項{string}并選擇操作", async function (itemName) {
let targetItem = model.getListItem(itemName);
await targetItem.click(0, 0, 2);
});
滾動清單
由于 Qt 目前不支援使用
vScroll()
和
hScroll
方法進行垂直和水準滾動,但我們還可以采用其它的方法可以進行滾動,例如:模拟按鍵(方向鍵和 PageUp/PageDown 鍵)進行滾動和翻頁、使用滾動條按鈕進行翻頁、使用 drag&drop 進行拖拽/滑屏操作。
這裡使用滾動條的按鈕滾動的方式考慮的場景————隻有一條垂直的滾動條,比較簡單。有些時候,還會出現水準的滾動條,這個時候就要區分滾動條進行操作。由于 Qt 的元件唯一辨別符較少,是以在識别時還要加上 index
屬性。是以如果是這種情況,可以使用另一種方法,即模拟按鍵的方式滾動。
Given("使用模拟按鍵進行滾動和翻頁", async function () {
let targetList = model.getList("List");
await targetList.click(); // 聚焦目标控件,使模拟按鍵對控件生效
await targetList.pressKeys("{PGDN}"); // 使用PageDown按鍵翻頁
await targetList.pressKeys('{DOWN 10}'); // 使用下箭頭向下滾動, 這裡按了10次
});
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');
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);
});
Then("使用drag與drop方法進行拖拽或滑屏操作", async function () {
let scrollBlock = model.getGeneric('Position');
await scrollBlock.drag();
await scrollBlock.drop(0, 40);
});
Then("使用vScroll和hScroll進行滾動(Qt暫不支援)", async function () {
let targetList = model.getList('List');
try {
await targetList.vScroll(100);
await targetList.hScroll(100);
} catch(err) {
console.warn("There is no function named `vScroll` and `hScroll`", err);
}
});
搜尋後選中目标選項
本次演練中還有兩個操作,一個是搜尋框的輸入,另一個是在搜尋結果中檢索是否有滿足條件的項。
- 搜尋框的輸入:搜尋框本質上是一個文本輸入框,是以可以使用
方法輸入指定字元串。由于這個應用會自動的搜尋,如果是需要另外輸入set()
Enter鍵信号觸發搜尋的,可以通過在輸入值後追加“~”符号來輸入Enter鍵,更多特殊按鍵的資訊可以查閱附錄:輸入鍵對應表[3]。ENTER
- 檢索結果:前面提到過,由于滾動視窗的原理,隻能擷取到目前可見的 ListItem 項,是以檢索搜尋結果,需要一邊滾動一邊判斷目前頁中是否有目标選項。這裡有兩個關鍵點:
- 擷取目前頁所有的 ListItem 項,并判斷是否含有目标項;
- 通過判斷滾動後,目前頁的最後一項是否和滾動前的最後一項相同,如果相同則代表已經滾動到最後一頁,也就是沒有找到目标項。
下面是這兩個操作的步驟定義代碼:
When("在搜尋框中輸入路徑{string}", async function (path) {
let searchBox = model.getEdit("Directory:");
await searchBox.click();
await searchBox.set(path);
assert.equal(await searchBox.value(), path);
});
Then("判斷搜尋結果中是否存在目标項{string}", async function (itemName) {
let list = await model.getList('List');
let lastItemName = '';
let foundFlag = false;
let name = '';
await list.click();
while(true) {
let items = await list.getControls({type: 'ListItem'});
for(let i=0; i let item = items[i];
name = await item.name();
if (name == itemName){
await item.click(1,1,1);
foundFlag = true;
}
}
if (foundFlag) break;
if (lastItemName == name) {
throw "List is end, item not found!";
break;
}
lastItemName = name;
await list.pressKeys('{PGDN}');
};
await Util.delay(3000);
});
總結
文中的代碼可在CukeTest中執行。可以到http://cuketest.com下載下傳CukeTest。
針對 ListView 的自動化操作的難點大多集中在滾動清單和内容查找上,針對清單項的自動化操作相對沒有特别需要注意的地方。文中使用的動态檢索所有控件的功能在很多複雜的、有挑戰性的自動化場景下可以作為 workaround 的方案。
參考資料
[1]
演練:操作Qt應用中的Table: http://www.cuketest.com/zh-cn/qt/walk/walk_qt_table.html
[2]
測試對象: http://www.cuketest.com/zh-cn/misc/glossary.html#測試對象
[3]
附錄:輸入鍵對應表: http://www.cuketest.com/zh-cn/misc/key_codes.html