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