Why You Can't Test a Chrome Extension the Normal Way

English 简体中文 繁体中文 Tiếng Việt
Summary

Testing Chrome extensions presents unique challenges due to their architecture, where popups are browser chrome, service workers are ephemeral, and content scripts run in isolated JavaScript contexts. Traditional automation tools like Puppeteer and Playwright struggle with these distinctions, and the removal of the `--load-extension` flag from branded Chrome builds further complicates direct testing. A new approach involves using Chrome for Testing builds and leveraging the `manifest.json` to generate initial smoke tests, with tools like `pikabo` enabling targeted assertions across different extension components like the popup, worker, and content scripts. This allows for more robust testing by directly interacting with extension surfaces and capturing events, including downloads initiated from the worker, and provides a permission audit to ensure declared permissions are actually exercised.

我构建浏览器扩展已有数年: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.storagechrome.runtimein: popup 将弹出窗口视为一个真实的文档。对于 MV2,还有 sidepaneldevtoolsoffscreenbackground。能够说明一个步骤作用于扩展程序的哪个部分是缺失的东西。

对于导出文件的扩展,下载会从任何来源捕获,包括从工作线程调用且没有页面参与的 chrome.downloads.download()。页面从未看到它,所以页面级别的自动化无法做到。然后 assertPdf 读取文件:页数、几何形状、字节大小、生产者。

一个设置说明,因为它立即困扰了我。如果你的扩展在仓库根目录有 manifest.json,不要在其中 npm installnode_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 或更高版本。如果你维护一个扩展程序,并且因为工具似乎不存在而推迟了测试,那么这就是它填补的特定空白。我很想听听在我的扩展程序之外还有什么会出问题。

TESTING OPEN-SOURCE AUTOMATION CHROME-EXTENSION MANIFEST-V3 PLAYWRIGHT SERVICE-WORKER

  RELATED

  COMMENT

1
Anonymous
Aug 17, 2026 at 4:20 am
Hi there!