头图

背景

基于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 管理等更底层的调试功能。

在常见基础操作上命名稍微有点区别:

功能BiDiCDP
导航browsingContext.navigatePage.navigate
执行jsscript.evaluateRuntime.evaluate

具体文档可以参考cdpBiDi

让pywebview支持CDP或BiDi

首先官网web engine 列举了pywebview在不同平台下的web engine

PlatformCodeRendererProviderBrowser compatibility
Android WebKit Ever - green Chromium
GTKgtkWebKitWebKit2 (minimum version >2.2)
macOS WebKitWebKit.WKWebView (bundled with OS)
QTqtWebKitQtWebEngine / QtWebKit
WindowsedgechromiumChromium> .NET Framework 4.6.2 and Edge Runtime installedEver - green Chromium
WindowscefCEFCEF PythonChrome 66
WindowsmshtmlMSHTMLDEPRECATED Internet Explorer MSHTMLIE11 (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包很方便,但基于图像识别不同操作系统会有差异,识别率要微调,不能做到一端开发,多端执行。但好处是跨平台,可以操作任何桌面应用。

附录

框架对比

特性/方面PlaywrightPuppeteerSeleniumCypressSikuli
开发者微软谷歌最初是内部测试工具,后发展为广泛使用项目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, .NETJavaScript (有非官方的 Python 端口 Pyppeteer)Java, Python, Ruby, C#, JavaScript,以及通过客户端语言绑定支持 Go, Haskell, PHP, Perl, R, Dart 等JavaScriptJava、Python等
浏览器功能使用修补的浏览器版本,支持多版本浏览器测试,多上下文浏览,支持浏览器扩展浏览器功能相对落后-支持调试工具,可实时查看测试步骤不依赖浏览器,可操作任何桌面应用
社区支持相对较新,不如 Puppeteer 广泛成熟的社区,出色的文档,更成熟的生态系统拥有庞大且活跃的社区,以及大量深入的文档有活跃的社区和丰富的插件社区相对较小
网络抓取支持功能上略有优势,补充功能多官方支持,有网络抓取教程-不专注于网络抓取-
设备模拟开箱即用的设备模拟跨浏览器测试未明确提及,可能需要额外配置-支持设备模拟可模拟鼠标键盘操作任何设备上的应用
维护多版本支持可能增加维护难度,未来可能出现重大变化相对稳定-易于维护,测试用例编写简洁图像识别可能因界面变化需要更新图像模板
速度通常比 Selenium 快-相对较慢,更适合小型到中型的抓取项目测试执行速度快图像识别有一定性能开销
架构基于响应事件的解耦系统 (事件驱动架构),异步通信-通过交换 JSON 有效负载使用 HTTP 与 Web 驱动程序交互 (JSON Wire 协议),可能产生延迟基于Node.js,与浏览器直接通信基于图像识别算法
WebDriver 管理内置驱动程序,实现更简单-需要为每个浏览器安装特定的 WebDriver (Selenium Manager 正在尝试解决这个问题)自动管理浏览器驱动不涉及WebDriver
无头浏览器能力支持无头浏览,可以模拟用户操作,解决动态页面抓取问题-支持无头浏览,可以模拟用户操作,解决动态页面抓取问题支持无头模式运行测试可操作无头应用(如命令行工具)
易用性更容易实现-相对复杂,需要配置 WebDriver测试用例编写简单,易于上手图像识别配置相对复杂
更新程度较新-较老更新频繁,功能不断完善更新相对较慢
适用场景需要跨浏览器支持,需要多种语言支持,需要更强大的通用网络自动化解决方案;项目需求可通过其支持的语言和浏览器满足时,可获得快速、高效且易于实现的无头浏览器需要大量同行指导,只使用 Chrome,时间有限,需要成熟的社区和文档需要灵活性,希望采用非常具体的浏览器和编程语言组合;考虑到在线可访问的资源范围,可能是学习使用无头浏览器进行网页抓取的有用工具前端项目的快速测试,注重开发调试体验需要对桌面应用进行自动化操作,尤其是界面元素难以用代码定位的情况
决策因素先前的熟悉程度在决策中起着重要作用先前的熟悉程度在决策中起着重要作用-项目前端技术栈和对测试速度的要求应用类型和自动化操作的定位方式
定义跨浏览器的 Web 自动化库,旨在简化端到端测试-专用于跨浏览器测试和自动化的开源框架用于现代Web应用的快速、简单、可靠的测试框架基于图像识别的自动化测试和操作工具

seasonley
618 声望695 粉丝

一切皆数据