背景
在 基於pywebview搭建企業級桌面端 這篇中記錄了多種webview桌面端的框架,並記錄了pywebview的使用和相關的坑。本篇記錄如何在pywebview中實現自動化測試。
由於pywebview沒有提供測試套件,只有幾個基礎的操作接口,所以需要結合一些測試框架來實現。
如果pywebview中不涉及bridge通信,那可以當做普通瀏覽器來測試,以上框架都支持。但pywebview中涉及bridge通信,那有特定接口無法模擬,比如自實現的系統文件操作或者窗體調整等超出了瀏覽器的功能,只能考慮變通的方式,比如CDP(chrome devtools protocol) 或者 BiDi (browser-to-browser protocol) 或者圖形化識別。
CDP和BiDi
兩者都是用過websocket通信,差異體現在:
- 設計目標:BiDi 協議旨在成為一種通用的、標準化的瀏覽器自動化和調試協議,注重跨瀏覽器的兼容性和簡潔性。CDP 最初是為 Chrome 瀏覽器的開發者工具調試功能設計的,更側重於滿足 Chrome 瀏覽器的特定調試需求,功能豐富且細緻,但相對複雜,並且主要適用於基於 Chromium 內核的瀏覽器。
- 功能範圍:BiDi 協議提供了一些高級的、通用的功能,如頁面導航、元素交互、腳本執行、監聽 DOM 事件、捕獲 JavaScript 錯誤和控制枱消息、記錄網絡流量等。CDP 除了包含上述功能外,還提供了對瀏覽器渲染引擎、緩存策略、Cookie 管理等更底層的調試功能。
在常見基礎操作上命名稍微有點區別:
| 功能 | BiDi | CDP |
|---|---|---|
| 導航 | browsingContext.navigate | Page.navigate |
| 執行js | script.evaluate | Runtime.evaluate |
具體文檔可以參考cdp 和 BiDi
讓pywebview支持CDP或BiDi
首先官網web engine 列舉了pywebview在不同平台下的web engine
| Platform | Code | Renderer | Provider | Browser compatibility |
|---|---|---|---|---|
| Android | WebKit | Ever - green Chromium | ||
| GTK | gtk | WebKit | WebKit2 (minimum version >2.2) | |
| macOS | WebKit | WebKit.WKWebView (bundled with OS) | ||
| QT | qt | WebKit | QtWebEngine / QtWebKit | |
| Windows | edgechromium | Chromium | > .NET Framework 4.6.2 and Edge Runtime installed | Ever - green Chromium |
| Windows | cef | CEF | CEF Python | Chrome 66 |
| Windows | mshtml | MSHTML | DEPRECATED Internet Explorer MSHTML | IE11 (Windows 10/8/7) |
查看changelog 發現 pywebview 5.4增加特性:
-
windows下支持 EdgeChromium通過添加遠程調試支持設置webview.settings['REMOTE_DEBUGGING_PORT'] 來打開cdp.源碼位置
#https://github.com/r0x0r/pywebview/blob/master/webview/platforms/edgechromium.py if webview_settings['REMOTE_DEBUGGING_PORT'] is not None: props.AdditionalBrowserArguments += f' --remote-debugging-port={webview_settings["REMOTE_DEBUGGING_PORT"]}'
但是實測發現即使設置了也沒有效果,端口並不通。
隨後查找其他方式,避免直接修改庫源碼。發現支持通過環境變量來設置,回退到pywebivew 4.3.3並啓動前執行
os.environ["WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"] = "--remote-debugging-port=9222"
至此selenium,playwright,pyppeteer都可以使用cdp操作了。
代碼示例
各大框架比對見附錄。考慮到playwright支持瀏覽器廣,上層sdk支持多種語言,且測試報告,調試,錄製,vscode插件等都支持,這裏以playwright為例。
線性執行
playwright默認是併發執行的,假設應用場景是這個桌面應用不支持多開,或者測試場景是線性執行,比如登錄,操作,退出。需要做如下配置:
- playwright.config.js配置
workers: 1使得單個線程執行 -
多個test用serial 標記,使得test之間串行執行
test.describe.serial('測試集合',()=>{ test.describe('測試A',()=>{}) test.describe('測試B',()=>{}) }) -
用一個page實例,避免每次重連
let page; test.beforeAll(async () => { const browser = await playwright.chromium.connectOverCDP('http://127.0.0.1:9222'); const defaultContext = browser.contexts()[0]; page = defaultContext.pages()[0]; });文件
-
獲取本地路徑
const directory = path.join(__dirname, `../fixtures/test.json`);模擬pywebview.api
-
執行腳本覆蓋pywebview的bridge通信
注意變量只能通過函數入參的形式傳入,因為可執行的腳本作用域在瀏覽器而不是測試腳本,兩者是隔離的
await page.evaluate((directory) => { // @ts-ignore window.pywebview = { api: { choosePath: () => Promise.resolve(JSON.stringify({ status: 'ok', directory })), getFileSize: () => Promise.resolve(1024) } }; }, directory);操作行為
-
常見操作
await page.goto('http://localhost:5000/'); await page.getByText('選擇文件').click() await page.waitForTimeout(4000); await page.getByRole('radio', { name: config.radioValue, exact: true }).check(); await page.locator('select').selectOption('value') await page.locator('.col:last-child').textContent();//querySelector await page.locator('//div[@class=".btn" and text()="確定"]').click();//xpath -
可選的操作
try { // 等待 1 秒,如果元素出現則點擊,超時則跳過 await page.waitForSelector(':text("×")', { timeout: 1000 }); await page.getByText('×').click(); } catch (error) { }
斷言
@playwright/test 是 Playwright 官方提供的測試框架,它自帶的 expect 斷言風格簡潔,與 Playwright 的 API 配合良好。
const { test, expect } = require('@playwright/test');
test('測試頁面標題', async ({ page }) => {
// 導航到頁面
await page.goto('https://www.example.com');
// 斷言頁面標題是否包含指定字符串
await expect(page).toHaveTitle(/Example/);
// 獲取頁面上某個元素(假設存在一個id為myElement的元素)
const element = page.locator('#myElement');
// 斷言元素是否可見
await expect(element).toBeVisible();
// 斷言元素的文本內容
const text = await element.textContent();
await expect(text).toContain('Some text');
});
測試報告
playwright提供了豐富的報告選項,包括 HTML、JSON等格式,可以自定義報告的樣式和內容。
// playwright.config.js
reporter: [
['json', { outputFile: 'test-results.json' }],
['html',{open: 'never'}],
]
上述配置會生成一個包含測試結果的 HTML 報告,以及一個 JSON 文件,其中包含測試結果的詳細信息。
如果接入CI,可以通過json獲取關鍵信息,便於判斷計數。這邊給出關鍵部分代碼
// 初始化計數器
let allCount = 0;
let passedCount = 0;
let failedCount = 0;
let flakyCount = 0;
let skippedCount = 0;
// 遞歸函數,用於遍歷所有測試用例
function traverseSuites(suites) {
suites.forEach((suite) => {
if (suite.specs) {
suite.specs.forEach((spec) => {
spec.tests.forEach((test) => {
allCount++;
const result = test.results[0];
if (result.status === "passed") {
passedCount++;
} else if (
result.status === "timedOut" ||
result.status === "failed"
) {
failedCount++;
} else if (result.status === "flaky") {
flakyCount++;
} else if (result.status === "skipped") {
skippedCount++;
}
});
});
}
if (suite.suites) {
traverseSuites(suite.suites);
}
});
}
錄製腳本
官方提供的錄製是彈出瀏覽器,不能直接在cdp的場景下錄製。所以不適用。但能通過 --ui的方式審查元素,比純手動簡單一些。
關於圖像識別的自動化測試
此外還調研了sikuli 基於圖像識別的自動化測試框架,直接執行sikuli的jar包很方便,但基於圖像識別不同操作系統會有差異,識別率要微調,不能做到一端開發,多端執行。但好處是跨平台,可以操作任何桌面應用。
附錄
框架對比
| 特性/方面 | Playwright | Puppeteer | Selenium | Cypress | Sikuli |
|---|---|---|---|---|---|
| 開發者 | 微軟 | 谷歌 | 最初是內部測試工具,後發展為廣泛使用項目 | Cypress.io團隊 | Ramanathan Guha和Tobias Grosser |
| 發佈時間 | 2020年1月 | 2017年 (旨在彌補 Selenium 的不足) | 2004年 | 2017年 | 2009年 |
| 流行度 | 近期發展勢頭強勁,但之前不如 Puppeteer | 過去一年更受歡迎,但 Playwright 正在趕超 | 擁有龐大且活躍的社區 | 在前端開發者中較受歡迎 | 相對小眾,主要用於特定場景 |
| 核心功能 | 自動化網頁交互,提取目標數據 | 自動化網頁交互,提取目標數據 | 跨瀏覽器測試、自動化、網頁抓取 | 端到端測試、集成測試等 | 基於圖像識別的自動化操作 |
| 自動等待功能 | 具備,模仿人類用户行為,避免被檢測為機器人 | 缺乏便利性,需要手動設置計時器 (例如 Page.waitForSelector()) |
- | 自動等待元素可交互,無需手動設置等待時間 | 基於圖像識別,等待目標圖像出現 |
| 反爬機制繞過 | 需要與第三方服務集成 (例如代理、AI解決方案) | 需要與第三方服務集成 (例如代理、AI解決方案) | - | - | 可繞過部分基於元素定位的反爬機制 |
| 客户端支持 | 異步和同步客户端 | 僅異步客户端 | - | 僅支持JavaScript編寫測試用例 | 支持Java、Python等 |
| 跨瀏覽器支持 | 強大,支持 Chrome/Chromium, WebKit (Safari), Firefox | 主要支持 Chrome/Chromium,對 Firefox/Edge 的支持處於實驗階段 | 多種瀏覽器選項,但需要為每個瀏覽器安裝特定的 WebDriver (Selenium Manager 嘗試解決,仍在 Beta 階段) | 主要支持Chrome、Firefox等主流瀏覽器 | 跨平台,可在Windows、Mac、Linux上運行 |
| 語言支持 | Python, Java, JavaScript, TypeScript, .NET | JavaScript (有非官方的 Python 端口 Pyppeteer) | Java, Python, Ruby, C#, JavaScript,以及通過客户端語言綁定支持 Go, Haskell, PHP, Perl, R, Dart 等 | JavaScript | Java、Python等 |
| 瀏覽器功能 | 使用修補的瀏覽器版本,支持多版本瀏覽器測試,多上下文瀏覽,支持瀏覽器擴展 | 瀏覽器功能相對落後 | - | 支持調試工具,可實時查看測試步驟 | 不依賴瀏覽器,可操作任何桌面應用 |
| 社區支持 | 相對較新,不如 Puppeteer 廣泛 | 成熟的社區,出色的文檔,更成熟的生態系統 | 擁有龐大且活躍的社區,以及大量深入的文檔 | 有活躍的社區和豐富的插件 | 社區相對較小 |
| 網絡抓取支持 | 功能上略有優勢,補充功能多 | 官方支持,有網絡抓取教程 | - | 不專注於網絡抓取 | - |
| 設備模擬 | 開箱即用的設備模擬跨瀏覽器測試 | 未明確提及,可能需要額外配置 | - | 支持設備模擬 | 可模擬鼠標鍵盤操作任何設備上的應用 |
| 維護 | 多版本支持可能增加維護難度,未來可能出現重大變化 | 相對穩定 | - | 易於維護,測試用例編寫簡潔 | 圖像識別可能因界面變化需要更新圖像模板 |
| 速度 | 通常比 Selenium 快 | - | 相對較慢,更適合小型到中型的抓取項目 | 測試執行速度快 | 圖像識別有一定性能開銷 |
| 架構 | 基於響應事件的解耦系統 (事件驅動架構),異步通信 | - | 通過交換 JSON 有效負載使用 HTTP 與 Web 驅動程序交互 (JSON Wire 協議),可能產生延遲 | 基於Node.js,與瀏覽器直接通信 | 基於圖像識別算法 |
| WebDriver 管理 | 內置驅動程序,實現更簡單 | - | 需要為每個瀏覽器安裝特定的 WebDriver (Selenium Manager 正在嘗試解決這個問題) | 自動管理瀏覽器驅動 | 不涉及WebDriver |
| 無頭瀏覽器能力 | 支持無頭瀏覽,可以模擬用户操作,解決動態頁面抓取問題 | - | 支持無頭瀏覽,可以模擬用户操作,解決動態頁面抓取問題 | 支持無頭模式運行測試 | 可操作無頭應用(如命令行工具) |
| 易用性 | 更容易實現 | - | 相對複雜,需要配置 WebDriver | 測試用例編寫簡單,易於上手 | 圖像識別配置相對複雜 |
| 更新程度 | 較新 | - | 較老 | 更新頻繁,功能不斷完善 | 更新相對較慢 |
| 適用場景 | 需要跨瀏覽器支持,需要多種語言支持,需要更強大的通用網絡自動化解決方案;項目需求可通過其支持的語言和瀏覽器滿足時,可獲得快速、高效且易於實現的無頭瀏覽器 | 需要大量同行指導,只使用 Chrome,時間有限,需要成熟的社區和文檔 | 需要靈活性,希望採用非常具體的瀏覽器和編程語言組合;考慮到在線可訪問的資源範圍,可能是學習使用無頭瀏覽器進行網頁抓取的有用工具 | 前端項目的快速測試,注重開發調試體驗 | 需要對桌面應用進行自動化操作,尤其是界面元素難以用代碼定位的情況 |
| 決策因素 | 先前的熟悉程度在決策中起着重要作用 | 先前的熟悉程度在決策中起着重要作用 | - | 項目前端技術棧和對測試速度的要求 | 應用類型和自動化操作的定位方式 |
| 定義 | 跨瀏覽器的 Web 自動化庫,旨在簡化端到端測試 | - | 專用於跨瀏覽器測試和自動化的開源框架 | 用於現代Web應用的快速、簡單、可靠的測試框架 | 基於圖像識別的自動化測試和操作工具 |