首页
/ Puppeteer Coverage.stopCSSCoverage() 深度解析:如何获取页面样式的精确使用范围

Puppeteer Coverage.stopCSSCoverage() 深度解析:如何获取页面样式的精确使用范围

2026-09-06 13:53:19作者:秋泉律Samson

本文围绕 Puppeteer API 文档中的 Coverage.stopCSSCoverage() 方法展开,它是 Puppeteer 代码覆盖率(Code Coverage)能力的核心终点:调用它即可拿到所有样式表的 CSS 覆盖率报告数组。读完本文,你将掌握该方法的签名与返回值结构、典型调用流程(与 startCSSCoverage 配对使用)、基于 CDP 协议(CSS.startRuleUsageTracking 等)的底层实现原理,以及如何用测试用例验证 ranges 的准确语义。

方法签名与返回值

官方 API 文档(见 stopCSSCoverage 文档页)给出的签名为:

class Coverage {
  stopCSSCoverage(): Promise<CoverageEntry[]>;
}
  • 无参数:该方法不接受任何配置,配置项全部在配套的 startCSSCoverage(options) 中传入;
  • 返回值:一个 Promise,解析为 CoverageEntry[],即所有样式表(stylesheets)的覆盖率报告数组;
  • 文档备注(Remarks):CSS Coverage doesn't include dynamically injected style tags without sourceURLs(CSS 覆盖率不包含没有 sourceURL 的动态注入 style 标签)。这一限制在源码中有直接体现,下文会展开。

CoverageEntry 的完整结构定义在 Coverage 类实现,文档见 CoverageEntry 接口页

export interface CoverageEntry {
  /** 样式表或脚本的 URL */
  url: string;
  /** 样式表或脚本的完整文本内容 */
  text: string;
  /** 被覆盖的范围,表示为起止偏移位置 */
  ranges: Array<{start: number; end: number}>;
}

三个字段组合起来的语义是:在 text 这个 CSS 源码字符串中,ranges 里每个 {start, end} 区间(半开区间)标记的是实际被浏览器应用过的 CSS 片段(例如某条完整规则)。text.substring(start, end) 可以直接截取出"被使用"的 CSS 片段。

典型使用流程:start 与 stop 配对

stopCSSCoverage() 必须与 startCSSCoverage() 成对使用:先开启追踪,触发页面行为(导航、交互、样式重计算),再调用 stop 收集结果并自动关闭追踪。来自 Coverage 类文档的官方示例,展示了同时收集 JS 与 CSS 覆盖率并计算"初始执行代码字节占比"的完整流程:

// 同时开启 JavaScript 和 CSS 覆盖率
await Promise.all([
  page.coverage.startJSCoverage(),
  page.coverage.startCSSCoverage(),
]);
// 导航到页面
await page.goto('https://example.com');
// 同时关闭 JavaScript 和 CSS 覆盖率
const [jsCoverage, cssCoverage] = await Promise.all([
  page.coverage.stopJSCoverage(),
  page.coverage.stopCSSCoverage(),
]);
let totalBytes = 0;
let usedBytes = 0;
const coverage = [...jsCoverage, ...cssCoverage];
for (const entry of coverage) {
  totalBytes += entry.text.length;
  for (const range of entry.ranges) usedBytes += range.end - range.start - 1;
}
console.log(`Bytes used: ${(usedBytes / totalBytes) * 100}%`);

需要留意官方示例中的细节:统计 CSS 使用字节时用的是 range.end - range.start - 1(JS 与 CSS 条目统一处理),而 ranges 本身按半开区间理解(substring(start, end) 截取即可),两者相差 1 个字节属于示例的口径处理。

开启阶段的可配置项

startCSSCoverage 的完整签名与参数(见 startCSSCoverage 文档页):

class Coverage {
  startCSSCoverage(options?: CSSCoverageOptions): Promise<void>;
}

CSSCoverageOptions 只有一个选项(见 CSSCoverageOptions 定义):

参数 类型 默认值 说明
resetOnNavigation boolean true 是否在每次导航后重置覆盖率。true 时只统计导航之后的样式表;false 时累积跨导航的样式表记录

源码中 CSSCoverage.start() 的默认值解构 const {resetOnNavigation = true} = options 印证了该默认值(CSSCoverage.start)。此外 start 内部会先断言 assert(!this.#enabled, 'CSSCoverage is already enabled'),即同一 Page 上不能重复开启 CSS 追踪,重复调用会直接抛出断言错误。

源码实现:CDP 协议如何支撑 stopCSSCoverage

stopCSSCoverage()Coverage 类 中只是一层薄薄的封装:

async stopCSSCoverage(): Promise<CoverageEntry[]> {
  return await this.#cssCoverage.stop();
}

真正的逻辑全部位于 CSSCoverage 类中,其生命周期由三个 CDP 命令驱动:

1. start 阶段(开启追踪)CSSCoverage.start):

await Promise.all([
  this.#client.send('DOM.enable'),
  this.#client.send('CSS.enable'),
  this.#client.send('CSS.startRuleUsageTracking'),
]);

同时注册两个事件监听:CSS.styleSheetAddedRuntime.executionContextsCleared。前者负责在每张样式表加载时记录其 URL 与全文(为 stop 阶段的报告做准备);后者在导航清空执行上下文时,按 resetOnNavigation 决定是否清空已收集的样式表映射。

2. 样式表收集:sourceURL 是关键过滤条件CSSCoverage.#onStyleSheet):

async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
  const header = event.header;
  // Ignore anonymous scripts
  if (!header.sourceURL) {
    return;
  }
  const response = await this.#client.send('CSS.getStyleSheetText', {
    styleSheetId: header.styleSheetId,
  });
  this.#stylesheetURLs.set(header.styleSheetId, header.sourceURL);
  this.#stylesheetSources.set(header.styleSheetId, response.text);
}

这里 if (!header.sourceURL) return; 正是文档 Remarks 中"不包含没有 sourceURL 的动态注入 style 标签"这一限制的根源——通过 page.addStyleTag({content: ...}) 之类方式注入、且没有 /*# sourceURL=xxx.css */ 注释的样式表,从一开始就不会进入收集映射,stop 时自然也不会出现在结果数组里。

3. stop 阶段(收集与聚合)CSSCoverage.stop):

async stop(): Promise<CoverageEntry[]> {
  assert(this.#enabled, 'CSSCoverage is not enabled');
  this.#enabled = false;
  const ruleTrackingResponse = await this.#client.send(
    'CSS.stopRuleUsageTracking',
  );
  await Promise.all([
    this.#client.send('CSS.disable'),
    this.#client.send('DOM.disable'),
  ]);
  // ... 按 styleSheetId 聚合 ruleUsage,产出 CoverageEntry[]
}

从源码结构看,stop() 的完整链路是:

  1. 断言追踪必须处于开启状态(未 start 就 stop 会抛出 CSSCoverage is not enabled);
  2. 发送 CSS.stopRuleUsageTracking,拿到浏览器返回的 ruleUsage 原始列表,每条含 styleSheetIdstartOffsetendOffsetused
  3. 关闭 CSSDOM 域,释放事件订阅;
  4. styleSheetId 聚合原始区间,未使用的规则以 count: 0 记录,已使用的以 count: 1 记录;
  5. 遍历 start 阶段收集的 #stylesheetURLs 映射,将原始区间交给 convertToDisjointRanges 转换为不相交区间后,组装成 {url, ranges, text} 条目返回。

值得注意的语义:结果是"按已收集的样式表"生成的——即使某张样式表没有任何规则被使用,它依然会出现在返回数组中,只是 ranges 为空数组(详见后文测试用例)。

区间算法:convertToDisjointRanges

CSS 规则追踪返回的原始区间是嵌套的(一条规则的区间包含其声明、选择器等子区间),直接输出会大量重叠。convertToDisjointRanges 用一个经典的"扫描线 + 命中计数栈"算法把嵌套区间化简为不相交区间:

  • 为每个区间生成 start/end 两个端点,按偏移排序(同偏移时 end 点优先、长区间 start 优先),保证端点序列构成合法的"括号序列";
  • hitCountStack 维护当前命中计数,只有栈顶计数 > 0 的区段才被认为是"被覆盖";
  • 相邻的命中区段会合并(lastResult.end === lastOffset 时直接延长),最后过滤掉空区间。

这也解释了测试中断言的精确偏移:对于 div { color: green; } 这样的源码,返回的是 {start: 1, end: 22} 这类可直接 substring 取用的边界。该算法在 JS 覆盖率(stopJSCoverage)与 CSS 覆盖率中是共用的。

测试用例验证:ranges 的准确语义

仓库的 CSS 覆盖率测试stopCSSCoverage() 的边界行为做了系统验证,值得逐条对照理解:

测试场景 验证点 源码位置
should work 单张内联样式表,ranges 精确等于 [{start: 1, end: 22}],且 text.substring(1, 22) 恰为 div { color: green; } test/src/coverage.test.ts#L188-L201
should report sourceURLs /*# sourceURL=nicename.css */ 的样式表,其 url 字段即 sourceURL 而非页面 URL test/src/coverage.test.ts#L202-L210
should report multiple stylesheets 多张外部样式表各自独立成条 test/src/coverage.test.ts#L211-L223
should report stylesheets that have no coverage 无规则被使用的样式表仍在结果中,ranges 长度为 0 test/src/coverage.test.ts#L224-L233
should work with media queries 媒体查询产生多个不相交区间,如 [{start: 8, end: 15}, {start: 17, end: 38}] test/src/coverage.test.ts#L234-L246
should work with empty stylesheets 空样式表返回 text: '' 的条目,不报错 test/src/coverage.test.ts#L257-L265
should ignore injected stylesheets page.addStyleTag({content}) 注入且无 sourceURL 的样式,最终 coverage 长度为 0,与文档 Remarks 完全对应 test/src/coverage.test.ts#L266-L278
resetOnNavigation(两个方向) false 时跨导航累计 2 张样式表;默认 true 时导航后清空、最终为空数组 test/src/coverage.test.ts#L297-L316

此外 should work with complicated usecases 用 golden 文件 csscoverage-involved.txt 锁定了复杂用例(嵌套媒体查询 + 外部样式表)的完整输出,作为回归基准。

实用建议与使用限制

综合文档与源码,使用 stopCSSCoverage() 时建议关注以下几点:

  1. 配对调用与幂等性startCSSCoveragestopCSSCoverage 各调用一次即可;未 start 就 stop 会触发断言异常,重复 start 同样会抛错。
  2. 动态样式要"可追溯":想让运行时注入的样式进入报告,需在 CSS 文本中写入 /*# sourceURL=xxx.css */ 注释(测试 should report sourceURLs 已验证该机制);否则该样式表会被静默忽略。
  3. 跨导航统计:若需要统计 SPA 多路由或多次 page.goto 之后所有样式表的累计使用情况,请以 startCSSCoverage({resetOnNavigation: false}) 开启。
  4. 与 JS 覆盖率组合page.coverage 同时提供 JS 侧的 startJSCoverage/stopJSCoverage,官方推荐的写法是 Promise.all 成对启停(见上文示例)。JS 侧的 JSCoverageEntry 还额外携带可选的 rawScriptCoverage(原始 V8 数据),而 CSS 侧的返回始终是基础 CoverageEntry 三字段结构。
  5. 下游工具:如需把结果转成 Istanbul 格式的覆盖率报告,官方文档推荐使用配套工具 puppeteer-to-istanbul(Coverage 类文档 的 Remarks 中有说明),它消费的就是 stopCSSCoverage/stopJSCoverage 产出的 {url, ranges, text} 结构。

Coverage 类通过 page.coverage 属性暴露(定义见 Page 抽象),构造器标记为 @internal,第三方代码应始终经由 page.coverage 访问,而不是直接实例化——这与 API 文档页 Coverage 类说明 的约束一致。

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

项目优选

收起
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++
915
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