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 窗口,因为弹出窗口、页面和 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.storagechrome.runtimein: popup 将弹出窗口视为真实文档。对于 MV2,还有 sidepaneldevtoolsoffscreenbackground。能够说明步骤作用于扩展程序的哪个部分是缺失的功能。

对于导出文件的扩展程序,下载会从任何来源捕获,包括 Worker 中调用且没有页面参与的 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 文件,只支持未打包的目录。文档外部的浏览器界面仍然无法点击——工具栏图标菜单、拼图块溢出、原生权限提示——尽管这些界面触发的所有内容都可以通过弹出窗口文档、选项页面或 Worker 来访问。manifest.commands 中声明的键盘快捷键无法作为真实的按键事件传递,因此您可以通过 Worker 来分派处理程序。根据以上所有内容,品牌 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!