这是怎么回事?
从 Manifest V2 到 Manifest V3 的过渡会发生根本性的变化。在 Manifest V2 中,扩展程序位于后台页面中。后台网页用于管理扩展程序与网页之间的通信。Manifest V3 改用 Service Worker。
在这篇博文中,我们深入探讨了测试扩展 Service Worker 的问题。特别是,我们研究了如何确保我们的产品在 Service Worker 被暂停时正常工作。
我们是谁?
eyeo 是一家致力于为用户、浏览器、广告客户和发布商提供平衡且可持续的在线价值交换的公司。我们在全球拥有超过 3 亿广告过滤用户,他们允许展示 Acceptable Ads,即一项独立广告标准,用于确定广告是否可接受且不具干扰性。
我们的 Extension Engine 团队负责提供广告过滤技术,为市场上一些最热门的广告拦截浏览器扩展程序(如 AdBlock 和 Adblock Plus)提供支持,在全球拥有 1.1 亿多用户。此外,我们还以开源库的形式提供这项技术,供其他广告过滤浏览器扩展程序使用。
什么是 Service Worker?
扩展程序 Service Worker 是浏览器扩展程序的中心事件处理脚本。它们在后台独立运行。一般来说,这没什么问题。在新 Service Worker 中,我们可以在后台页面上完成大部分需要执行的操作。但与后台网页相比,存在一些变化:
- Service Worker 在不使用时终止。这需要我们保留应用状态,而不是依赖于全局变量。这意味着,我们系统的所有入口点都必须准备好在系统初始化之前被调用。
- 在等待任何异步回调之前,必须先附加事件监听器。已暂停的 Service Worker 仍然可以接收其已订阅的事件。如果事件监听器未在事件循环的第一轮注册,则不会在该事件唤醒 Service Worker 时收到事件。
- 闲置终止功能可能会在计时器完成之前使计时器遭到中断。
Service Worker 何时暂停?
对于 Chrome 119,我们遇到的是 Service Worker 会被暂停:
- 30 秒后未收到事件或调用扩展程序 API。
- 如果开发者工具是打开的,或者您使用的是基于 ChromeDriver 的测试库(请参阅功能请求),则永远不会这样做。
- 如果您在 chrome://serviceworker-internals 中点击 Stop,
如需了解最新信息,请参阅 Service Worker 生命周期。
为什么测试此问题会带来问题?
理想情况下,如果提供关于“如何高效测试 Service Worker”的官方指导或提供有效测试示例,会很有帮助。在测试 Service Worker 的过程中,我们遇到了一些挑战:
- 我们的测试扩展程序中有状态。当 Service Worker 停止时,我们会丢失其状态和注册的事件。如何在测试流程中保留数据?
- 如果 Service Worker 可能在任何时候被暂停,我们需要测试所有功能在中断后是否都能正常工作。
- 即使我们在测试中引入了一种随机挂起 Service Worker 的机制,浏览器中也没有可用于轻松暂停 Service Worker 的 API。我们已要求 W3C 团队添加此功能,但这是一个持续的讨论。
测试 Service Worker 暂停
我们已尝试过几种在测试期间触发 Service Worker 暂停的方法:
方法 | 此方法存在的问题 |
等待任意时长(例如 30 秒) | 这会使测试速度缓慢且不可靠,尤其是在运行多项测试时。在使用 WebDriver 时,WebDriver 无法正常工作,因为 WebDriver 使用 Chrome 的开发者工具 API,而且 Service Worker 在打开开发者工具时不会被暂停。即使我们可以绕过它,我们仍然必须检查 Service Worker 是否已暂停,我们没有办法做到这一点。 |
在 Service Worker 中运行无限循环 | 根据相关规范,这可能导致终止,具体取决于浏览器如何实现此功能。在这种情况下,Chrome 不会终止 Service Worker,因此我们不能测试 Service Worker 暂停时的情况。 |
在 Service Worker 中显示一条消息,用于检查其是否已被暂停 | 发送消息可唤醒 Service Worker。此方法可用于检查 Service Worker 是否处于休眠状态,但它会破坏需要在暂停 Service Worker 后立即执行检查的测试的结果。 |
使用 chrome.processes.terminate() 终止 Service Worker 进程 | 扩展程序的 Service Worker 会与扩展程序的其他部分共享进程,因此,使用 chrome.process.terminate() 或 Chrome 的进程管理器 GUI 可终止此进程,而不仅会终止该 Service Worker,还会终止所有扩展程序页面。 |
最后,我们进行了一项测试,该测试通过让 Selenium WebDriver 打开 chrome://serviceworker-internals/ 并点击 Service Worker 的“stop”按钮,来检查代码如何响应挂起的 Service Worker。
这是目前最好的选择,但目前并不理想,因为我们的 Mocha 测试(在扩展程序页面上运行) 无法自行执行此操作,因此它们需要传回我们的 WebDriver 节点程序。这意味着,这些测试无法仅使用扩展程序运行;必须使用 Selenium WebDriver 触发。
下图说明了我们如何通过不同的流与浏览器 API 进行通信,以及添加“挂起 Service Worker”机制对它的影响。
在挂起 Service Worker(蓝色)的新流程中,我们添加了 Selenium WebDriver 以通过界面“点击”挂起,这会在浏览器 API 中触发操作。
值得一提的是,在 Chrome 错误下,使用 Selenium WebDriver 执行此操作会导致 Service Worker 无法重新启动。此问题已在 Chrome 116 中得到解决,幸运的是,还有一种权宜解决方法:将 Chrome 设置为在每个标签页上自动打开 DevTools,即可让 Service Worker 正确启动。
这是我们在测试时采用的方法,尽管这种方法并不理想,因为点击该按钮可能不是稳定的 API,并且打开开发者工具(对于旧版浏览器)似乎会降低性能。
如何涵盖全部功能?模糊测试
有了用于测试暂停的机制后,我们必须决定如何将其插入到自动化测试套件中。我们在以下环境中运行标准测试:在每次与后台页面交互之前,WebDriver 会点击 chrome://serviceworker-internals/ 页面上的 Stop,从而暂停 Service Worker。
我们会运行大多数(而非所有)测试,因为挂起机制并不完全稳定,有时还会导致不稳定。此外,在模糊模式下运行所有测试套件需要耗费大量时间。因此,我们没有涵盖所有“类似”情况,而是选择了最关键的路径,在模糊测试模式下进行测试。值得一提的是,在“模糊”模式下运行功能测试意味着我们必须增加测试的超时,因为暂停和重启 Service Worker 需要额外的时间。
这些测试作为粗粒度的首次通过检查,可以突出显示代码失败的许多地方,但不一定能发现 Service Worker 挂起可能导致故障的所有细微变化。
在内部,我们将这类测试称为“模糊测试”。传统上,模糊测试是指在程序中抛出无效输入,并确保其可以合理响应,或者至少不会崩溃。在我们的案例中,“无效输入”是指随时被暂停的 Service Worker,而我们预期的“合理行为”是指广告过滤功能必须继续像以前一样工作。这实际上并不是无效的输入,因为这是 Manifest V3 中的预期行为,但在 Manifest V2 中是无效的,所以它看起来是合理的术语。
摘要
Service Worker 是 Manifest V3 中最大的变化之一(除了 declarativeNetRequest 规则之外)。迁移到 Manifest V3 可能需要对浏览器扩展程序进行很多代码更改,并采用新的测试方法。它还要求具有持久状态的扩展程序的开发者准备其扩展程序,以便妥善处理意外的 Service Worker 暂停。
遗憾的是,目前没有 API 能够以适合我们用例的简单方式处理中止。由于我们想在早期阶段针对挂起机制测试扩展程序代码库的稳健性,因此必须着手解决。其他面临类似挑战的扩展程序开发者可以使用这种权宜解决方法,虽然这种方案在开发和维护阶段非常耗时,但值得一试,因为这样可以确保我们的扩展程序能够在定期暂停 Service Worker 的环境中成功运行。
尽管已经提供了对测试 Service Worker 挂起方法的基本支持,但通过扩展程序内为测试 Service Worker 提供了更好的平台支持,这是我们未来非常希望实现的,因为它可以大大减少我们的测试执行时间和维护工作量。