在 Selenium WebDriver 中等待加载复杂 JavaScript 页面的策略
1. 引言
使用 Selenium 测试应用时,等待页面完全加载在现代页面中更为复杂,因为这些页面重度依赖 JavaScript 进行渲染。这类页面在初始 HTML 加载后会发起异步调用并操作 DOM。因此,仅检查页面是否加载完成不足以确保应用已准备好进行交互。
引入任意延迟是一种常见但有问题的选择。如果页面加载速度快于延迟时间,测试会浪费时间;反之,如果页面因网络状况或服务器响应时间加载变慢,测试可能会失败。
在本教程中,我们将探索不同的策略,在不引入任意延迟的情况下等待 JavaScript 密集型页面在 Selenium WebDriver 中完全加载。
2. 问题与设置
我们将页面定义为在当前视图的所有异步操作完成时加载完毕。这包括 HTTP 请求、动态 DOM 更新以及框架特定的处理。导航到新视图被视为一次新的页面加载,需要其自身的等待策略。
Selenium 提供了两种主要的等待策略。隐式等待设置元素查找的全局超时。而显式等待则使用 WebDriverWait 实例等待特定条件。我们只使用显式等待,它提供了对检查条件的精确控制。
2.1. 创建辅助类
让我们创建一个封装等待逻辑的辅助类。它包装了一个 WebDriverWait 实例,并需要一个 timeout 值,以便在意外场景下测试不会无限期等待:
我们将所有不同的等待策略添加到这个类中,使其易于复用。我们只需要 WebDriverWait 的 until() 方法,它会轮询条件直到返回 true 或超时。 这个条件写成一个lambda 表达式,接收当前 driver:
虽然我们不需要特定的 driver 实现,但唯一的要求是它实现了 JavaScriptExecutor 接口。 我们需要这个是因为我们想要查询的状态只能通过浏览器的 JavaScript 运行时访问,而不是通过标准的 WebDriver API。
另外,由于某些检查依赖于特定框架,本文中的大多数方法以 typeof someFrameworkObject === 'undefined' 这样的检查开头。如果框架不存在,条件立即返回 true,使得该方法可以在任何页面上安全调用,并且在链式调用时很有用。
3. 等待文档就绪状态
最基本的检查是检查浏览器的 document.readyState 属性。当页面及其所有资源加载完成后,该属性变为 complete。 这是 driver.get() 的默认行为。对于 SPA 导航(例如点击一个改变视图但不进行完整页面重新加载的链接后)非常有用。
让我们在 PageLoadHelper 中添加一个方法,执行脚本并等待返回我们期望的结果:
虽然这覆盖了初始 HTML 页面加载,但它不适用于 DOM 就绪后运行的异步 JavaScript。
4. 等待 jQuery AJAX 调用完成
许多应用仍使用 jQuery 进行 AJAX 请求。它维护了一个内部计数器 jQuery.active,用于跟踪待处理的请求数量。当该值归零时,所有请求已完成。
让我们添加一个方法,检查是否有待处理的请求:
它首先检查页面上是否存在 jQuery。如果不存在,条件立即返回 true。这种守卫在构建通用测试套件且并非所有页面都使用 jQuery 时很有用。
然而,如果在我们调用时 AJAX 请求尚未开始,条件会看到 jQuery.active === 0 并在任何请求触发前返回。因此,让我们创建另一个方法,等待请求开始:
然后我们可以将这两个方法串联起来,确保请求开始后再等待它们完成。 但即使采用这种方法,如果所有请求在我们有机会观察到 jQuery.active 增加之前就已经触发并完成,我们将不得不等待直到 Selenium 超时。
5. 等待现代 Angular 应用稳定
Angular 2+ 使用 RxJS 进行 HTTP 调用,并提供了一个可测试性 API,允许我们检查应用是否稳定,即是否所有待处理请求已完成:
我们检查 getAllAngularTestabilities 是否为 undefined,以防范未来的 API 更改。
6. 等待 React 应用就绪
React 应用没有内置的 JavaScript API 可以通用地测试应用就绪状态,因此我们必须逐个场景处理。一种常见的方法是只在完成所有必要请求后才使某个元素可见。
一个简单的“完成”标志,为按钮添加一个 CSS 类就足够了:
然后,我们可以添加一个通用方法来等待元素可点击:
最后,我们可以通过传递元素的 ID 来测试它:
这种方法不限于 React,可用于任何页面。 最重要的是,done 标志必须在应用真正准备好交互时设置。如果完成初始请求集触发了额外的请求,那么该标志必须在整个链完成后才设置为 true。
7. 在 Window 对象中注入自定义标志
在现代 JavaScript 应用中,我们通常使用 Promise 进行请求,并且可以使用 Promise.all() 函数等待一组 Promise 完成。最重要的是,如果我们无法使用通用工具,我们总是可以修改 window 变量以包含我们自己的标志。
所以,如果我们知道应用中最慢的部分是完成一组 Promise,并且在此之前我们无法可靠地使用应用,那么一个替代方案是创建一个简单的监听器,添加一个标志表示应用就绪:
将变量初始化为 false 很重要,这样在读取其值时不会得到 undefined。 然后,在使用 Selenium 测试应用时,我们可以等待这个标志出现:
这适用于任何在现代浏览器中运行的应用。
8. 修补 Fetch API
现代 JavaScript 应用的另一种常见方法是使用 fetch,它也没有提供标准的计数待处理请求的方法。因此,一个选择是自行实现。我们可以通过替换 fetch 实现为一个自定义版本,该版本递增全局 fetchPending 计数器,并将实际执行委托给原始实现:
这必须在我们要跟踪的请求触发之前调用。然后,每个使用 fetch 命令发出的请求都变得可跟踪(即使在错误时也是如此,因为我们在 finally 块中递减它)。 最后,我们可以等待 fetchPending 归零,就像我们的 appReady 解决方案一样。
对于在页面加载时自动触发的请求,拦截必须在导航之前进行。Selenium 现在通过 Chrome DevTools 协议 支持这一点,但这超出了本文的范围。
9. 总结
我们讨论的策略针对不同的场景。让我们比较一下:
在实践中,组合策略效果最佳:从 waitForDocumentReady() 开始,应用框架特定的检查,然后确认一个有意义的元素可交互。
10. 结论
在本文中,我们探讨了使用 Selenium 等待 JavaScript 密集型页面完全加载的几种策略。我们从基本的 document.readyState 检查开始,并介绍了 Selenium 内置的 WebDriverWait 来等待元素状态。我们还扩展了方法以处理 jQuery AJAX 调用、现代 Angular 应用、React 组件的替代方案以及纯 JavaScript 应用。
没有单一的策略能覆盖所有情况,正确的选择取决于应用所使用的技术以及我们对源代码的控制程度。使用像 PageLoadHelper 这样的辅助类可以简化在测试套件中组合和复用策略。
一如既往,源代码可在 GitHub 上获取。
《使用 Selenium WebDriver 等待复杂 JavaScript 页面加载》 最初发表于 Baeldung。