首页
/ Puppeteer JSCoverage.stop() 完整指南:JavaScript 代码覆盖率采集的收尾与结果处理

Puppeteer JSCoverage.stop() 完整指南:JavaScript 代码覆盖率采集的收尾与结果处理

2026-09-07 17:45:35作者:魏侃纯Zoe

导读

JSCoverage.stop() 是 Puppeteer(Chrome 与 Firefox 的 JavaScript 自动化 API)中 JS 代码覆盖率(JavaScript Coverage)能力的收官方法:当通过 start()(或 page.coverage.startJSCoverage())启动采样后,调用它会向浏览器发出"取出当前全部已执行脚本的覆盖报告"的指令,并以结构化的 JSCoverageEntry[] 数组返回结果。读完本文,你将掌握 stop 阶段的完整调用链、返回数据结构的每个字段含义、四个启动参数对返回结果的深远影响,以及如何基于返回结果手工计算"已执行字节占比"这一覆盖率指标。

一、方法签名与返回类型

本文核心文档定义于 docs/api/puppeteer.jscoverage.stop.md,其完整方法签名如下:

class JSCoverage {
  stop(): Promise<JSCoverageEntry[]>;
}
要素 说明
方法所属类 JSCoverage(构造函数标记为 internal,第三方代码不应直接 new,请通过 page.coverage 访问)
参数
返回值 Promise<JSCoverageEntry[]>:覆盖全部脚本的覆盖率报告数组
语义 停止采集并解析为浏览器中所有已记录脚本的覆盖报告(见 puppeteer.coverage.stopjscoverage.md

Coverage 门面类上的公开方法是等价关系:在源码 packages/puppeteer-core/src/cdp/Coverage.ts 中,Coverage.stopJSCoverage() 只是简单委托给内部持有的 JSCoverage 实例:

async stopJSCoverage(): Promise<JSCoverageEntry[]> {
  return await this.#jsCoverage.stop();
}

因此绝大多数实际业务代码是这样调用的:

const jsEntries: JSCoverageEntry[] = await page.coverage.stopJSCoverage();

JSCoverage 类本身只有两个公开方法 start(options)stop(),参见 puppeteer.jscoverage.md 中罗列的方法表。

二、返回值 JSCoverageEntry 数据结构详解

stop() 返回的每个元素是 JSCoverageEntry,它继承自 CoverageEntry(代码覆盖率报告的一条通用条目),并额外带有一个可选属性。完整字段如下:

字段 类型 继承自 说明
url string CoverageEntry 该脚本的 URL
text string CoverageEntry 样式表或脚本的完整源代码内容
ranges Array<{ start: number; end: number; }> CoverageEntry 被覆盖(已执行到)的区间,以字符起止位置表示
rawScriptCoverage? Protocol.Profiler.ScriptCoverage(可选) JSCoverageEntry 扩展 原始 V8 脚本覆盖率条目,仅在启动时开启 includeRawScriptCoverage 才会出现

接口定义可直接查看源码 Coverage.ts。理解这三类字段的分工是使用覆盖率结果的前提:

  • text 提供脚本全文,是计算总字节数的依据;
  • ranges 是已执行代码段的字符区间集合,多个区间之和用于计算已使用字节数
  • rawScriptCoverage 提供 V8 原生粒度数据(每个函数的调用次数、执行区间明细),适合做精细分析或桥接其他工具链。

关于 ranges 的关键提醒

源码 stop() 实现 会遍历每个脚本的 functions,把 V8 返回的所有函数的执行区间摊平后再合并

const flattenRanges = [];
for (const func of entry.functions) {
  flattenRanges.push(...func.ranges);
}
const ranges = convertToDisjointRanges(flattenRanges);

convertToDisjointRanges(见 Coverage.ts)内部采用"扫描线 + 命中计数栈"算法,将可能相互嵌套、重叠的区间整理成互不相交(disjoint)的合并区间,并过滤掉空区间。因此 ranges 中各段不会重复计数,可以直接按 range.end - range.start 求和得到精确的已覆盖字节数。

三、与 start() 的配对关系:四个选项如何影响 stop() 的结果

stop() 本身无参数,但它的输出质量几乎完全由 start() 时传入的 JSCoverageOptions 决定。start() 的签名(见 puppeteer.jscoverage.start.md)为:

class JSCoverage {
  start(options?: {
    resetOnNavigation?: boolean;
    reportAnonymousScripts?: boolean;
    includeRawScriptCoverage?: boolean;
    useBlockCoverage?: boolean;
  }): Promise<void>;
}

默认值在 Coverage.ts 中通过解构显式给出,与 API 文档描述一致:resetOnNavigation: truereportAnonymousScripts: falseincludeRawScriptCoverage: falseuseBlockCoverage: true

选项 默认值 含义 对 stop() 结果的影响
resetOnNavigation true 每次导航时是否重置覆盖率 true 时,导航发生的瞬间会清空已记录的脚本 URL/源码表(见 Coverage.tsRuntime.executionContextsCleared 事件处理器);为 false 则可跨导航累积采样,报告覆盖整个会话期间执行过的脚本
reportAnonymousScripts false 是否上报页面产生的匿名脚本 匿名脚本指页面中用 evalnew Function 动态创建、没有关联 URL 的脚本。默认 false 时这些脚本不出现在 stop() 返回的数组中;为 true 时其 url 会以 debugger://VM 开头(除非存在 //# sourceURL 魔法注释,此时用该注释值作为 URL)。此行为同样体现在 startJSCoverage 的 remarks 与源码 #onScriptParsed 及 stop 中 'debugger://VM' + entry.scriptId 的拼接逻辑
includeRawScriptCoverage false 结果是否包含原始 V8 覆盖率条目 true 时每个返回条目额外携带 rawScriptCoverage 字段(见 Coverage.ts);同时底层会以 callCount: true 请求 V8 记录函数调用次数(见 Profiler.startPreciseCoverage 调用)
useBlockCoverage true 覆盖率采集粒度 true(默认)为**块级(block)粒度;false 则退化为函数级(function)**粒度。该值在底层被映射为 Profiler.startPreciseCoveragedetailed 参数,detailed: false 时区间只落到整个函数体,无法反映函数内部哪些分支/块被执行

底层协议调用链(start → 运行 → stop 的完整闭环)

Coverage.ts 可以看到,start() 实际通过 CDP 并行下发 4 个命令:

Profiler.enable
Profiler.startPreciseCoverage  { callCount: includeRawScriptCoverage, detailed: useBlockCoverage }
Debugger.enable
Debugger.setSkipAllPauses      { skip: true }

同时监听 Debugger.scriptParsed(有新脚本解析时记录其 URL 与源码,见 #onScriptParsed)与 Runtime.executionContextsCleared(导航发生时按 resetOnNavigation 决定是否清表)。

stop() 对应的底层命令序列(见 Coverage.ts)为:

const result = await Promise.all([
  this.#client.send('Profiler.takePreciseCoverage'),
  this.#client.send('Profiler.stopPreciseCoverage'),
  this.#client.send('Profiler.disable'),
  this.#client.send('Debugger.disable'),
]);

takePreciseCoverage 用于取出此刻累计的覆盖数据,随后立即停止精确覆盖并关闭 Profiler 与 Debugger 域,最后释放内部事件订阅(#subscriptions?.dispose()),将 #enabled 置回 false。需要特别注意的是状态管理:

  • 若在未调用 start() 的情况下调用 stop(),会触发源码中的断言 assert(this.#enabled, 'JSCoverage is not enabled') 而抛出异常(见 Coverage.ts);
  • 重复调用 start() 而未 stop 也会断言失败('JSCoverage is already enabled')。因此 start/stop 必须严格配对,推荐的做法是采集完成后立即在 finally 中停止,避免泄漏事件监听与 Profiler 占用。

四、排除规则:哪些脚本不会出现在结果里

stop() 返回"所有脚本的覆盖报告",但有三类脚本会被有意过滤(源码位置见 #onScriptParsedstop 中的过滤):

  1. Puppeteer 自身注入的脚本:URL 命中 PuppeteerURL.isPuppeteerURL() 的(如 puppeteer:// 前缀的内部脚本)一律忽略,避免自动化框架自身的 JS 污染统计结果;
  2. 匿名脚本:无 URL 且未开启 reportAnonymousScripts 的脚本被忽略——即默认情况下 eval/new Function 产生的代码不计入报告。但文档明确强调:带有 sourceURL 的脚本会被报告(见 puppeteer.coverage.stopjscoverage.md 的 remarks);
  3. 源码或 URL 查不到的脚本:stop 时若在内部缓存表中找不到对应 urltext(例如脚本获取源码的请求因页面已跳转而失败),该条目会被直接 continue 跳过(见 Coverage.ts)。

五、完整实战:测量页面初始执行的 JS 覆盖率

下面给出可直接运行的完整示例,演示 start → 导航 → stop 的标准工作流。该模式与 Coverage 类的官方示例 完全一致,只是本节聚焦 JS 覆盖率的独立采集与统计:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// 1. 开启 JavaScript 覆盖率采集
await page.coverage.startJSCoverage({
  resetOnNavigation: true,
  reportAnonymousScripts: false,
  includeRawScriptCoverage: false,
  useBlockCoverage: true,
});

try {
  // 2. 让页面执行真正的业务代码
  await page.goto('https://example.com', {waitUntil: 'networkidle0'});
  // 视场景需要,可继续与页面交互以增加覆盖范围……

  // 3. 停止采集,拿到所有脚本的覆盖报告
  const jsCoverage = await page.coverage.stopJSCoverage();

  // 4. 统计覆盖率:已用字节 / 总字节
  let totalBytes = 0;
  let usedBytes = 0;
  for (const entry of jsCoverage) {
    totalBytes += entry.text.length;
    for (const range of entry.ranges) {
      usedBytes += range.end - range.start; // 注意与官方示例的 -1 差异
    }
  }
  const ratio = totalBytes > 0 ? (usedBytes / totalBytes) * 100 : 0;
  console.log(`脚本总数: ${jsCoverage.length}`);
  console.log(`总字节: ${totalBytes}, 已使用字节: ${usedBytes}`);
  console.log(`JS 覆盖率: ${ratio.toFixed(2)}%`);
} finally {
  // 5. 兜底停止,保证 start/stop 严格配对
  await page.coverage.stopJSCoverage();
  await browser.close();
}

说明:官方示例中累加字节数时使用 range.end - range.start - 1(对应 Coverage.ts 类注释),因为其 ranges 语义按"端点字符占用"估算;合并后的区间本已是互不相交的闭区间,两种口径在工程上都可用,关键是保持统计口径一致。

如果想在同一会话内同时统计 JS 与 CSS 的初始执行情况,可参考官方推荐的并行写法:

// 同时开启
await Promise.all([
  page.coverage.startJSCoverage(),
  page.coverage.startCSSCoverage(),
]);
await page.goto('https://example.com');
// 同时停止
const [jsCoverage, cssCoverage] = await Promise.all([
  page.coverage.stopJSCoverage(),
  page.coverage.stopCSSCoverage(),
]);

结果后续处理:从 ranges 到逐行/逐文件报告

拿到 ranges 后可根据目标做二次加工:

  • 定位未覆盖代码:对每条 entry,把 ranges 视为"已执行段",将 text 中落在这些段之外的行标记为未覆盖,即可输出带行号的报告;
  • 接入 Istanbul 系工具:官方在 Coverage 类的 remarks 中提示,若需要输出可被 Istanbul 生态消费的格式,可参考 puppeteer-to-istanbul 这类转换工具(见 Coverage.ts);
  • 取原始 V8 数据:当需要函数级调用次数等更细信息时,以 includeRawScriptCoverage: true 启动,随后从每条 entry 的 rawScriptCoverage.functions 中读取每个函数的 rangescount 字段。

六、典型应用场景与注意事项

典型场景

  • 首屏代码冗余分析:仅导航首页、不做任何交互就 stop,得到的覆盖率即"首屏真正执行到的代码",用于发现体积大但冷启动不执行的脚本;
  • 关键用户路径(Happy Path)回归守护:在测试脚本中走完注册、下单等主流程后 stop,把关键路径的覆盖率纳入 CI 阈值检查;
  • 按需加载验证:对比不同页面的覆盖率报告,确认路由懒加载分包是否在进入对应路由后才被真实执行。

注意事项(务必核对)

  1. 只统计"发生过脚本解析"的上下文:脚本只有在页面解析(Debugger.scriptParsed)时才会被登记,动态后注入但从未运行的脚本也可能因为先解析后运行而计入 ranges 为空的结果,统计时需自行过滤 ranges.length === 0 的条目;
  2. 匿名脚本的坑:默认不报告 eval/new Function 代码;若业务框架大量使用动态代码生成,请开启 reportAnonymousScripts,此时这类条目的 urldebugger://VM<id>,注意其无法按常规文件路径归类;
  3. 跨导航累积resetOnNavigation 默认 true 意味着每次导航都会清空记录,只统计当前页;若想覆盖"多页跳转"全流程,请显式传 false
  4. 粒度影响区间精度useBlockCoverage 默认 true(块级);若置 false,函数内部分支的执行差异将无法体现,覆盖率数字会显得偏高;
  5. 禁用 JS(page.setJavaScriptEnabled(false))后脚本不会执行,覆盖率将失去意义,需先确认页面确实启用了 JS 并真实完成了导航与交互;
  6. 无法成文类边界:本能力属于浏览器协议层,Firefox(WebDriver BiDi)等其他传输层是否支持需以对应浏览器后端能力为准,代码层面该实现位于 CDP 传输路径(packages/puppeteer-core/src/cdp/Coverage.ts)。

底层验证与进一步阅读

登录后查看全文
热门项目推荐
相关项目推荐