几年来我一直在构建浏览器扩展程序:PageSaver,它将网页转换为 PDF 或图像,以及其他一些扩展。它们中的每一个都未经任何自动化测试就已发布。不是因为我不想进行测试,而是因为我已知的工具无法触及那些真正会出错的部分。
循环总是相同的。更改一个文件,打开 chrome://extensions,点击重新加载,点击工具栏图标,测试选项页面,然后打开三个独立的 DevTools 窗口,因为弹出窗口、页面和 Service Worker 各自维护自己的控制台。漏掉一个,bug 就会发布。
我终于坐下来认真解决这个问题,首先我必须接受的是,问题不在于生态系统的懒惰。测试扩展程序确实与测试 Web 应用程序不同,有三个具体方面。
扩展程序不是页面
Playwright 和 Puppeteer 非常出色,它们可以驱动页面。扩展程序大部分不是页面。
从弹出窗口开始。当您点击扩展程序的工具栏图标时,打开的不是一个标签页——而是浏览器界面,由浏览器在自动化工具知道如何寻址的页面层次结构之外渲染。没有标签页可以附加,也没有句柄可以抓取。您无法通过脚本点击它,任何巧妙的选择器工作都无法改变这一点。
后台更糟。根据 Manifest V3,它是一个 Service Worker,这意味着没有标签页、没有 DOM,也没有窗口。page.evaluate() 没有可以附加的对象,因为没有页面。Worker 在空闲时也会关闭,并在下一个事件发生时重新启动,因此即使是“它是否正在运行”也是一个带有时间戳的问题。
内容脚本看起来很简单,但实际上并非如此。它们运行在一个隔离的世界中:相同的 DOM,完全独立的 JavaScript 上下文。如果您的内容脚本设置了一个变量,而您在页面中评估一个表达式来读取它,您将得到 undefined。这两个上下文共享标记,仅此而已。
因此,弹出窗口无法点击,Worker 无法评估,内容脚本也无法从页面中检查。这构成了扩展程序的大部分。
然后 Chrome 拿走了前门
解决所有这些问题的旧方法是使用 --load-extension 启动浏览器并进行驱动。这不再奏效了。Google 在 Chrome 137 中移除了该标志,给出的理由是合理的:“它经常被滥用来将恶意和不受欢迎的软件加载到浏览器中。”
对于测试至关重要的部分在 chromium-extensions 组的公告中:
请注意,此更改仅适用于 Chrome 品牌版本。--load-extension 在非 Chrome 版本(如 Chromium 和 Chrome For Testing)中将继续按原样工作。
这一句话就是全部策略。该标志并没有消失,它只是从品牌 Chrome 中消失了。Google 为自动化发布的 Chrome for Testing 仍然支持它,Google 直接将其命名为要使用的工具。
网上有一些博文建议您可以使用 --disable-features=DisableLoadExtensionCommandLineSwitch 强制品牌 Chrome 恢复正常。不要依赖它。我在处理这个问题时测试了 Chrome 150.0.7871.187,无论是否有该标志,未打包的扩展程序都无法加载。--enable-unsafe-extension-debugging 也无法恢复它。那个逃生舱已经关闭了,而且它从一开始就没有被批准。
我的建议是停止将您桌面上的 Chrome 视为测试浏览器。使用 Chrome for Testing。这是支持的路径,是 Google 指向的路径,也是下个季度唯一仍然有效的路径。
您看不到的失败
这是让我确信需要真正工具而不是一堆脚本的一点。
MV3 Service Worker 的控制台仅在其检查器打开时存在。Chrome 不会为您缓冲该输出。因此,如果您的后台脚本在第一行就抛出错误——一个拼写错误、一个缺失的导入、一个您忘记声明权限的 API——您会打开弹出窗口,看到一个空白的矩形,检查页面控制台,一无所获,并且不知道任何东西抛出了错误。Worker 在您观看之前就已崩溃。
解决方法是在 Worker 启动之前通过 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 已经声明了您的弹出窗口、选项页面、内容脚本匹配模式以及您是否有 Service Worker。这足以生成一个真正的烟雾测试,而无需编写任何内容。
因此,我围绕这个想法构建了 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 以确认注入,并断言 Worker 已干净启动。生成的套件以 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 在 Service Worker 中运行一个步骤,其中包含 chrome.storage 和 chrome.runtime。in: popup 将弹出窗口视为真实文档。对于 MV2,还有 sidepanel、devtools、offscreen 和 background。能够说明步骤作用于扩展程序的哪个部分是缺失的功能。
对于导出文件的扩展程序,下载会从任何来源捕获,包括 Worker 中调用且没有页面参与的 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 文件,只支持未打包的目录。文档外部的浏览器界面仍然无法点击——工具栏图标菜单、拼图块溢出、原生权限提示——尽管这些界面触发的所有内容都可以通过弹出窗口文档、选项页面或 Worker 来访问。manifest.commands 中声明的键盘快捷键无法作为真实的按键事件传递,因此您可以通过 Worker 来分派处理程序。根据以上所有内容,品牌 Chrome 已被排除在外。
每次会话还会进行一次权限审核,将您的代码实际触及的 chrome.* 命名空间与 manifest 声明的进行比较。它将发现报告为未观察到而不是未使用,这很重要:仅在您的测试从未触及的路径上使用的权限与死代码看起来相同。一个打开弹出窗口的烟雾测试套件并不能证明任何关于权限的信息。报告会说明这一点,而不是让您删除您需要的东西。
pikabo 是 MIT 许可,在 npm 上以 pikabo 的名称提供,需要 Node 22 或更高版本。如果您维护一个扩展程序并且因为工具似乎不存在而推迟了测试,那么这正是它填补的空白。我很想听听我的扩展程序以外的其他扩展程序会遇到什么问题。