我构建浏览器扩展已有数年:PageSaver,可将网页转换为 PDF 或图像,以及其他一些扩展。它们每一个都未经任何自动化测试就已发布。并非我不想要测试,而是我已知的工具无法触及那些真正会出问题的部分。
循环总是相同的。更改一个文件,打开 chrome://extensions,点击重新加载,点击工具栏图标,测试选项页面,然后打开三个独立的 DevTools 窗口,因为弹出窗口、页面和服务工作线程各自维护着自己的控制台。漏掉一个,bug 就随之发布了。
我终于坐下来认真解决这个问题,首先我必须接受的是,问题不在于生态系统的懒惰。测试扩展程序确实与测试 Web 应用不同,主要有三个方面。
扩展程序不是一个页面
Playwright 和 Puppeteer 非常出色,它们可以驱动页面。但扩展程序大部分时间都不是一个页面。
从弹出窗口开始。当你点击扩展程序的工具栏图标时,打开的东西不是一个标签页——而是浏览器界面,由浏览器在自动化工具所知的页面层级之外渲染。没有标签页可以附加,也没有句柄可以抓取。你无法通过脚本点击它,任何巧妙的选择器技巧都无法改变这一点。
后台更糟。在 Manifest V3 下,它是一个服务工作线程,这意味着没有标签页,没有 DOM,也没有窗口。page.evaluate() 没有可以附加的对象,因为它没有页面。工作线程在空闲时也会关闭,并在下一个事件触发时重新启动,所以即使是“它是否正在运行”这个问题,也附带了时间戳。
内容脚本看起来是简单的情况,但事实并非如此。它们运行在一个隔离的环境中:与页面相同的 DOM,完全独立 JavaScript 上下文。如果你的内容脚本设置了一个变量,而你在页面中评估一个表达式来读取它,你会得到 undefined。这两个上下文共享标记,仅此而已。
所以弹出窗口无法点击,工作线程无法评估,内容脚本也无法从页面中检查。这几乎就是扩展程序的大部分了。
然后 Chrome 移除了前门
解决所有这些问题的旧方法是使用 --load-extension 启动浏览器并进行驱动。这不再奏效了。Google 在 Chrome 137 中移除了该标志,给出的理由是合理的:“它经常被滥用来将恶意和不受欢迎的软件加载到浏览器中。”
对于测试来说,重要的部分在 chromium-extensions 组的公告中:
请注意,此更改仅适用于 Chrome 品牌版本。--load-extension 在非 Chrome 版本(如 Chromium 和 Chrome For Testing)中将继续按原样工作。
这一句话就是全部策略。该标志并没有消失,它只是从品牌 Chrome 中消失了。Google Chrome for Testing——Google 专门为自动化发布的版本——仍然支持它,Google 也直接指明了这是应该去的地方。
网上有一些博文建议你可以通过 --disable-features=DisableLoadExtensionCommandLineSwitch 来强制品牌 Chrome 恢复正常。不要依赖这个。我在处理这个问题时测试了它,针对 Chrome 150.0.7871.187,无论是否有该标志,未打包的扩展都无法加载。--enable-unsafe-extension-debugging 也无法恢复它。那个逃生舱已经关闭了,而且它一开始就未经批准。
我的建议是停止将你任务栏上的 Chrome 视为你的测试浏览器。使用 Chrome for Testing。这是支持的路径,是 Google 指向的路径,也是下个季度唯一仍然有效的路径。
你看不见的失败
这是让我确信需要真正工具而不是一堆脚本的那一点。
MV3 服务工作线程的控制台仅在其检查器打开时存在。Chrome 不会为你缓冲该输出。所以,如果你的后台脚本在第一行就抛出错误——一个拼写错误,一个缺失的导入,一个你忘记声明权限的 API——你打开弹出窗口,看到一个空白的矩形,检查页面控制台,一无所获,完全不知道任何东西抛出了错误。工作线程在你观看之前就已经崩溃了。
解决方法是在工作线程启动之前通过 DevTools 协议进行连接,这样在它抛出错误时就已经在收集缓冲的输出了。这样做,同样的失败就会自行报告:
✗ service worker starts without errors
assertNoConsoleErrors: 2 console error(s):
[worker] ReferenceError: initialise is not defined
at chrome-extension://abc…/background.js:4:1
[worker] Error: Unhandled rejection: storage quota exceeded
at chrome-extension://abc…/background.js:31:16
文件和行号,来自一个从未运行足够长时间以手动显示堆栈跟踪的脚本。
第一个测试套件的大部分已经在你的 manifest 中了
这是让我感到惊讶的部分。manifest.json 已经声明了你的弹出窗口、你的选项页面、你的内容脚本匹配模式以及你是否有服务工作线程。这足以生成一个真正的冒烟测试,而无需编写任何代码。
所以我围绕这个想法构建了 pikabo。指向一个未打包的扩展:
$ npx pikabo explore --ext ./my-extension
my-extension v2.1.0 · MV3
popup: popup.html · options: options.html · service worker · 2 content script blocks · 5 permissions
Generated 5 smoke test(s) → tests/smoke.generated.yaml
my-extension smoke tests
✓ service worker starts without errors 12ms
✓ popup renders 431ms
✓ options page renders 318ms
✓ content script injects on github.com 772ms
✓ content script injects on gitlab.com 684ms
5 passed · 3.8s
html pikabo-results/report.html
这其中没有手动编写的测试。它读取了 manifest,在一个真实的 Chromium 中打开了每个声明的表面,检查了弹出窗口是否渲染了实际内容而不是空壳,导航到一个匹配每个内容脚本模式的 URL 以确认注入,并断言工作线程干净启动。生成的套件以 YAML 格式写入磁盘,以便你可以继续编辑它,而不是将其视为黑盒。
从那里你可以编写有意义的断言,有用的部分是每个表面都可以通过名称寻址:
name: PageSaver
tests:
- name: popup saves a PDF
steps:
- navigate: https://example.com
- openPopup
- click: "#save-pdf"
- waitForDownload: "*.pdf"
- assertStorage:
key: lastSave
exists: true
- assertNoConsoleErrors
in: worker 在服务工作线程内部运行一个步骤,其中包含 chrome.storage 和 chrome.runtime。in: popup 将弹出窗口视为一个真实的文档。对于 MV2,还有 sidepanel、devtools、offscreen 和 background。能够说明一个步骤作用于扩展程序的哪个部分是缺失的东西。
对于导出文件的扩展,下载会从任何来源捕获,包括从工作线程调用且没有页面参与的 chrome.downloads.download()。页面从未看到它,所以页面级别的自动化无法做到。然后 assertPdf 读取文件:页数、几何形状、字节大小、生产者。
一个设置说明,因为它立即困扰了我。如果你的扩展在仓库根目录有 manifest.json,不要在其中 npm install;node_modules 会放在 manifest 旁边,并被打包进你上传到 Web Store 的 ZIP 文件中。将测试放在自己的文件夹中,并指向扩展程序:
mkdir extension-tests && cd extension-tests
npm init -y && npm install --save-dev pikabo
npx playwright-core install chromium
npx pikabo run tests --ext ../my-extension
它不会做什么
清楚地说明边界,因为“测试通过”和“它能工作”之间的差距是信任丢失的地方。
不支持从 Web Store 安装的打包 .crx 文件,只支持未打包的目录。文档外部的浏览器界面仍然无法点击——工具栏图标菜单、拼图块溢出、原生权限提示——尽管这些表面触发的所有内容都可以通过弹出窗口文档、选项页面或工作线程访问。manifest.commands 中声明的键盘快捷键无法作为真实的按键事件传递,因此你需要通过工作线程来分派处理程序。并且如上所述,品牌 Chrome 不支持。
还有一个权限审计,在每次会话中运行,将你的代码实际触及的 chrome.* 命名空间与 manifest 声明的进行比较。它将发现报告为未观察到而不是未使用,这很重要:仅在你的测试从未触及的路径上使用的权限,看起来与死掉的权限相同。一个打开弹出窗口的冒烟测试并不能证明任何关于权限的事情。报告会说明这一点,而不是让你删除你需要的东西。
pikabo 是 MIT 许可,在 npm 上以 pikabo 的名义发布,需要 Node 22 或更高版本。如果你维护一个扩展程序,并且因为工具似乎不存在而推迟了测试,那么这就是它填补的特定空白。我很想听听在我的扩展程序之外还有什么会出问题。