首页
/ Puppeteer 加载与测试 Chrome 扩展的完整指南:启用扩展、后台上下文、Popup 与内容脚本实战

Puppeteer 加载与测试 Chrome 扩展的完整指南:启用扩展、后台上下文、Popup 与内容脚本实战

2026-09-07 17:41:34作者:鲍丁臣Ursa

本文是一份面向开发者的实战指南,围绕 Puppeteer 提供的 Chrome 扩展测试能力展开:如何通过 enableExtensions 启动带扩展的浏览器、如何在运行时动态安装与卸载扩展、如何定位 MV3 Service Worker / MV2 后台页、如何打开 Popup 与触发扩展动作,以及如何借助 extensionRealms 在内容脚本上下文中执行代码。读完本文,你将能够直接用 Puppeteer 写出可复用的 Chrome 扩展端到端测试,并在 docs/guides/chrome-extensions.md 之外,理解这些 API 在 packages/puppeteer-core/src/api/Browser.tsChromeLauncher.ts 等源码中的底层实现。

一、Puppeteer 扩展测试能力总览

Chrome 扩展由 Manifest 声明其能力,并由多种运行上下文组成:MV3(Manifest V3)以 Service Worker 作为后台、content_scripts 注入普通网页、action 点击后可能弹出 Popup 或打开页面。因此“测试扩展”远比“测试网页”复杂——既要控制浏览器加载扩展,又要在多个不同的 JS 执行上下文里注入断言。

Puppeteer 在官方文档中明确声明可用于测试 Chrome 扩展,其支持的完整操作集合为:

  • 启动浏览器时直接加载扩展(enableExtensions 接收扩展路径数组),或在运行中按需安装(browser.installExtension);
  • 列出、读取与卸载已安装扩展(browser.extensions、browser.uninstallExtension);
  • 获取扩展的 Service Worker(MV3)或后台页(MV2)句柄,并在其中执行代码;
  • 打开并测试 action 弹出的 Popup 页面;
  • 以编程方式触发扩展动作(page.triggerExtensionActionextension.triggerAction);
  • 定位扩展注入内容脚本后产生的 Realm,在内容脚本上下文中执行代码。

Extension 抽象类(源码见 packages/puppeteer-core/src/api/Extension.ts)即运行期扩展对象的公共模型:每个实例携带 idnameversionpathenabled 只读属性,并声明了 workers()(当前活跃的 Service Worker 列表)、pages()(当前可见的扩展页面列表)与 triggerAction(page) 三个抽象方法。注意该类在文档中标注为 @experimental,扩展相关 API 属于实验性能力,使用前请留意你所依赖的 Puppeteer 版本。

二、扩展支持在底层是如何实现的

要正确使用扩展测试能力,有必要先理解 Puppeteer 对浏览器默认参数的处理逻辑,这部分证据集中在 packages/puppeteer-core/src/node/ChromeLauncher.ts

Puppeteer 为浏览器进程拼装的默认启动参数中默认会携带 --disable-extensions,这正是扩展无法被加载的根本原因。其逻辑为:

const {enableExtensions = false, ...} = options;
// ...
if (!enableExtensions) {
  chromeArguments.push('--disable-extensions');
}

(见 ChromeLauncher.ts。)换言之,只要 enableExtensions 为真值,Puppeteer 就不再注入这一禁用参数,从而为扩展打开大门。参数类型在 LaunchOptions.ts 中被声明为:

/**
 * If `true`, avoids passing default arguments to the browser that would
 * prevent extensions from being enabled. Passing a list of strings will
 * load the provided paths as unpacked extensions.
 */
enableExtensions?: boolean | string[];

值得进一步说明的是 string[] 形态在底层并非“启动参数直接加载”,而是启动完成后逐个调用安装 API。在 BrowserLauncher.tslaunch() 收尾阶段可以看到:

if (Array.isArray(enableExtensions)) {
  await Promise.all([
    enableExtensions.map(path => {
      return browser.installExtension(path, {
        enabledInIncognito: extensionsEnabledInIncognito.includes(path),
      });
    }),
  ]);
}

这带来两个重要的工程含义:

  1. 传数组时 Puppeteer 会等待所有扩展安装完成后才继续返回 browser,因此首屏即能 waitForTarget 到后台上下文;
  2. 每个扩展是否在隐身模式(Incognito / off-the-record)下启用,由配套的 extensionsEnabledInIncognito?: string[](见 LaunchOptions.ts)指定,命中的路径会以 enabledInIncognito: true 完成安装。

也就是说,enableExtensions 的三种取值分别对应三种典型诉求:true = 允许扩展但不预先安装;路径数组 = 启动时同步安装若干扩展;extensionsEnabledInIncognito 数组 = 同时声明哪些扩展要进入隐身上下文。无论哪种方式,扩展都必须以未打包(unpacked)的目录形式提供,目录内需含 manifest.json

三、加载扩展的两种方式

3.1 通过 LaunchOptions 在启动时加载

最常见的做法是把扩展目录路径直接交给 launch(),Puppeteer 启动浏览器后会自动完成安装(其底层走的就是上一节的 browser.installExtension() 路径):

import puppeteer from 'puppeteer';
import path from 'path';

const pathToExtension = path.join(process.cwd(), 'my-extension');
const browser = await puppeteer.launch({
  enableExtensions: [pathToExtension],
});

3.2 在运行期动态安装

先以 enableExtensions: true 启动(仅保证不被 --disable-extensions 禁用),随后在任何时刻调用 browser.installExtension(path) 动态加载,并获得该扩展的 ID:

import puppeteer from 'puppeteer';
import path from 'path';

const pathToExtension = path.join(process.cwd(), 'my-extension');
const browser = await puppeteer.launch({
  enableExtensions: true,
});

const extensionId = await browser.installExtension(pathToExtension);

两种方式各有适用场景:静态方式适合“扩展固定不变、每次测试都从干净状态开始”的 CI 场景;动态方式适合“同一浏览器实例内串联测试多个扩展、按需注入”的场景。仓库测试 test/src/cdp/extensions.test.ts 正是以 setupSeparateTestBrowserHooks({enableExtensions: true}, {createContext: false}) 启动浏览器后,再逐条调用 browser.installExtension(extensionPath) 来验证 Service Worker、Popup、动作触发等行为的(其测试夹具位于 test/assets/simple-extensiontest/assets/extension-with-page)。

四、列出、读取与卸载扩展

安装之后,用 browser.extensions() 可拿到以扩展 ID 为键的 Map<string, Extension>,进而读取 nameversion 等元数据;browser.uninstallExtension(id) 则负责卸载:

const extensions = await browser.extensions();
const extension = extensions.get(extensionId);

console.log(extension?.name);
console.log(extension?.version);

await browser.uninstallExtension(extensionId);

在此基础上,Extension 抽象类(见 packages/puppeteer-core/src/api/Extension.ts)还进一步暴露了四个与运行实体相关的成员:

  • extension.id / name / version / path / enabled:标识与元数据,其中 enabled 表示扩展当前是否处于启用状态;
  • extension.workers():返回该扩展当前活跃的 Service Worker(WebWorker[]),便于批量断言或终止;
  • extension.pages():返回该扩展当前打开且可见的页面(Page[]),例如固定标签页(Tab)型扩展;
  • extension.triggerAction(page):等价于在指定页面上触发扩展的默认动作(详见后文)。

test/src/cdp/extensions.test.ts 中可以看到配套的验证方式:安装后 waitForTarget 该扩展的 service_worker target,卸载后再次检查 browser.targets(),确认与该扩展关联的 Service Worker target 已不再出现。

五、访问后台上下文:MV3 Service Worker 与 MV2 后台页

扩展的逻辑中枢并不在可见页面里,而在后台上下文中。Puppeteer 通过 browser.waitForTarget() 结合 target.type() 来捕获它,拿到 Target 后再转换为可执行句柄。

5.1 MV3 Service Worker

MV3 扩展以 Service Worker 为后台。下面示例假设你的扩展只产生一个 URL 以 background.js 结尾的 Service Worker:

import puppeteer from 'puppeteer';
import path from 'path';

const pathToExtension = path.join(process.cwd(), 'my-extension');
const browser = await puppeteer.launch({
  enableExtensions: [pathToExtension],
});

const workerTarget = await browser.waitForTarget(
  // Assumes that there is only one service worker created by the extension and its URL ends with background.js.
  target =>
    target.type() === 'service_worker' &&
    target.url().endsWith('background.js'),
);

const worker = await workerTarget.worker();

// Test the service worker.

await browser.close();

拿到 WebWorker 后,可以:

  • worker.evaluate(fn, ...args) 在扩展后台上下文中执行任意代码(读取全局状态、触发内部逻辑);
  • 配合 target 的生命周期进行“强制终止/重新唤醒”类测试——例如先断言扩展在空闲后被浏览器回收,再断言某个事件能把它重新唤醒。

仓库测试即演示了“在 Service Worker 中求值”的写法:安装 simple-extensionwaitForTarget、再 target.worker()evaluate(() => globalThis.MAGIC),断言结果为 42(见 test/src/cdp/extensions.test.ts)。

5.2 MV2 后台页

对于仍使用 MV2 的扩展,后台是一个真正的页面(background_page 类型 target),因此可以直接将其转为普通 Page 来操作,与测试普通页面完全一致:

import puppeteer from 'puppeteer';
import path from 'path';

const pathToExtension = path.join(process.cwd(), 'my-extension');
const browser = await puppeteer.launch({
  enableExtensions: [pathToExtension],
});
const backgroundPageTarget = await browser.waitForTarget(
  target => target.type() === 'background_page',
);
const backgroundPage = await backgroundPageTarget.page();

// Test the background page as you would any other page.

await browser.close();

六、测试 Popup:由 Service Worker 打开并捕获目标页

许多扩展通过点击工具栏图标弹出 Popup。Popup 本质是一个普通页面 target,但它由后台 Service Worker 调用 chrome.action.openPopup() 打开,因此测试流程分两步:先进入后台上下文触发打开,再用 waitForTarget 捕获 URL 以 popup.html 结尾的页面 target,最后通过 target.asPage() 拿页面句柄:

await worker.evaluate('chrome.action.openPopup();');

const popupTarget = await browser.waitForTarget(
  // Assumes that there is only one page with the URL ending with popup.html
  // and that is the popup created by the extension.
  target => target.type() === 'page' && target.url().endsWith('popup.html'),
);

const popupPage = await popupTarget.asPage();

// Test the popup page as you would any other page.

await browser.close();

worker 即上一节获取的 Service Worker 句柄(见 5.1)。捕获 Popup target 时的 url() 过滤通常以扩展 ID + popup.html 双条件为准,避免与普通页面混淆。

七、以编程方式触发扩展动作

真实用户是通过点击工具栏图标触发扩展 action 的。Puppeteer 提供两条等价的程序化触发入口,效果等同用户点击扩展按钮:

  • page.triggerExtensionAction(extension):在当前页面上触发指定扩展的默认动作(API 文档);
  • extension.triggerAction(page):从扩展对象一侧向指定页面发起同样触发。
const extensions = await browser.extensions();
const extension = extensions.get(extensionId);

// You can trigger the action for a specific extension on a page.
await page.triggerExtensionAction(extension);

// Alternatively, you can trigger it from the extension object itself.
await extension.triggerAction(page);

// If the action opens a popup, you can then wait for the popup target.
const popupTarget = await browser.waitForTarget(
  target =>
    target.type() === 'page' &&
    target.url().includes(extensionId) &&
    target.url().endsWith('popup.html'),
);

当扩展的 action 被配置为“打开 Popup”时,触发后立即 waitForTarget 即可拿到弹出页;当 action 被配置为执行脚本或打开新标签页时,则可继续用 waitForTarget/target.asPage() 处理后续目标。仓库在 test/src/cdp/extensions.test.ts 中对“page.triggerExtensionAction(extension) 触发 → waitForTarget 捕获 popup.html”的完整链路有端到端覆盖。

八、测试内容脚本:通过扩展 Realm 注入断言

内容脚本(content script)会按 Manifest 声明注入到匹配的普通网页中,因此测试方式非常直观:browser.newPage() 打开一个满足 matches 条件的页面,内容脚本便会随页面加载而注入(见 docs/guides/chrome-extensions.md)。

难点在于:内容脚本运行在隔离世界(isolated world)中,与页面主世界互不可见,Puppeteer 常规的 page.evaluate() 进不去。解决方案是使用扩展 Realm

  • page.extensionRealms()Page API,其实现为 mainFrame().extensionRealms() 的快捷方式,见 Frame.ts)返回页面上与各扩展关联的 Realm[]
  • 遍历 Realm 并调用 realm.extension()Realm API)拿回其所属扩展,与目标 extensionId 比对;
  • 命中后调用 realm.evaluate(...) 即可在内容脚本上下文内执行代码、访问其 DOM 与私有状态。
// Get the extension ID
const extensionId = await browser.installExtension(pathToExtension);

// Find the extension realm.
const realms = page.extensionRealms();
let extensionRealm;
for (const realm of realms) {
  const extension = await realm.extension();
  if (extension?.id === extensionId) {
    extensionRealm = realm;
    break;
  }
}

if (!extensionRealm) {
  throw new Error('Extension realm not found');
}

// Evaluate code in the content script context.
const result = await extensionRealm.evaluate(() => {
  return document.title;
});

这段代码实际验证了内容脚本侧的真实 DOM(示例返回 document.title)。实际项目中,extensionRealm.evaluate 更适合断言内容脚本注入的元素、事件监听副作用,或调用内容脚本内部暴露的调试钩子——这些都是主世界 page.evaluate 无法触及的。realm.extension() 返回可能为 null(例如不属于任何扩展的 Realm),因此官方示例使用 extension?.id === extensionId 的可空安全写法。该 API 相关实现逻辑可在 Frame.tsRealmPage.extensionRealms 中找到,cdp/IsolatedWorldcdp/Frame 也参与了该能力。

九、把零散能力组合成一条完整测试链路

将前文串联起来,一个典型 MV3 扩展的端到端测试通常呈现如下骨架:

  1. 启动puppeteer.launch({enableExtensions: [pathToExtension]})(或 enableExtensions: true 后运行期 installExtension);
  2. 取 ID:从 browser.installExtension() 返回值或 browser.extensions() 拿到 extensionId
  3. 后台waitForTarget 捕获 service_worker target → target.worker() 获得后台句柄,先做状态断言;
  4. 动作与 Popuppage.triggerExtensionAction(extension)(或 extension.triggerAction(page))→ waitForTarget 捕获 popup.htmlasPage() 后做 UI 断言;
  5. 内容脚本page.extensionRealms() 定位到目标扩展 Realm,在隔离世界中校验 DOM 副作用;
  6. 清理browser.uninstallExtension(extensionId)browser.close()

仓库还内置了一个可运行的参考实现 examples/puppeteer-in-extension/,内含 manifest.jsonbackground.jsiframe.htmlplayground.html,可直接作为“如何在扩展里跑 Puppeteer”或“如何组织扩展页面清单”的脚手架阅读;若要观察 Puppeteer 官方自测的扩展编排方式,可对照 test/src/cdp/extensions.test.ts 及其加载的 test/assets/simple-extensiontest/assets/extension-with-page 两个夹具目录。

十、注意事项与已知边界

  • 默认参数即开关:只要 enableExtensions 未开启,Puppeteer 的默认参数就会写入 --disable-extensionsChromeLauncher.ts),因此所有扩展用法都以此开关为前提;
  • 扩展必须是未打包目录enableExtensions 数组、installExtension 参数均指向包含 manifest.json 的本地目录,不支持 .crx 打包文件;
  • target 过滤要写准:扩展运行时会同时出现 service_workerbackground_page、普通 page(Popup/选项页/固定标签页)等不同 target,务必组合 target.type()target.url() 过滤,避免误捕获;
  • 上下文差异:Service Worker 用 worker.evaluate,Popup/后台页用 page.evaluate,内容脚本用 realm.evaluate,三者互不通用;
  • 实验性标记Extension 及其相关 API 在类型注释中标注 @experimental,接口可能在后续版本演进,升级 Puppeteer 时需留意 CHANGELOG.mdpackages/puppeteer-core/CHANGELOG.md
  • 查询优先级:本指南对应的官方原文 docs/guides/chrome-extensions.md 只描述 API 层面的使用方式;若你运行的浏览器/平台不在此 API 的支持范围,应以实测或该仓库的版本说明为准。各方法逐个的签名与类型定义可继续查阅 docs/api 目录下的 puppeteer.browser.installextension.mdpuppeteer.browser.extensions.mdpuppeteer.extension.*.mdpuppeteer.page.triggerextensionaction.mdpuppeteer.realm.extension.md 等文档页。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388