jsdom 测试体系全解析:web-platform-tests 与 Mocha 双框架的编写与运行指南
jsdom 测试体系全解析:web-platform-tests 与 Mocha 双框架的编写与运行指南
jsdom 是一个在 Node.js 中实现 DOM、HTML 等 Web 标准的 JavaScript 库,而它的测试体系直接服务于“与浏览器行为保持一致”这一核心使命。test/README.md 是理解这套测试体系的总入口:jsdom 同时使用两套测试框架——跨浏览器标准的 web-platform-tests(WPT) 与面向 jsdom 自身 API 的 Mocha。读完本文,你将掌握这两套框架的定位差异、测试文件的正确存放位置、从单条用例到全量套件的完整运行命令,以及如何利用 to-run.yaml、to-upstream-expectations.yaml 等配置文件管理“预期失败”的用例。
两套测试框架:WPT 与 Mocha 的分工
jsdom 项目采用两套测试框架,核心区别在于“测什么、从哪个角度测”:
| 框架 | 测试形态 | 运行环境 | 覆盖对象 |
|---|---|---|---|
| web-platform-tests | .html 文件,使用 testharness.js 报告结果 |
在 jsdom 创建的文档内部执行 | Web 平台特性(DOM、HTML、CSSOM、Fetch、XHR 等标准行为) |
| Mocha | .js 文件 |
在 Node.js 进程中以 Mocha 运行 | jsdom 自身对外暴露的 API(如 JSDOM() 构造函数、options 参数等) |
web-platform-tests:跨浏览器一致性测试
WPT 是浏览器厂商联合维护的跨浏览器测试库,jsdom 作为参与者,直接将这些 .html 测试文件在自己的实现内运行。这类测试与 jsdom 实现的具体 Web 平台类一一对应——例如 Attr、Node、Location、CSSStyleSheet 等类,其规格以 Web IDL 描述、实现位于 lib/jsdom/living/ 下对应的 *-impl.js,而 WPT 恰好提供了验证这些实现行为是否符合规范的最佳测试来源。
Mocha:jsdom API 的外向测试
Mocha 测试用于验证“只能在 jsdom API 层面完成、无法在真实浏览器中模拟”的场景。正如 test/api/README.md 所述,这类测试从“外部”视角测试 JSDOM 类对外呈现的公共 API(该 API 的权威文档是顶层 README),而非测试 jsdom 内部实现的 Web 平台特性。
新增测试放哪里:一条决定目录归属的核心原则
test/README.md 给出了一条非常明确的指引,值得所有贡献者牢记:
- Web 平台特性的测试 → 放入
test/web-platform-tests/to-upstream/子目录,这应当是绝大多数新测试的归属地; - jsdom API 自身的测试 → 放入
test/api/子目录; - 其余目录一概忽略("Ignore all the rest")。
这条规则的背后逻辑很清晰:jsdom 的绝大多数功能工作都是在补齐 Web 平台标准,因此以 WPT 格式编写测试才是主流。只有像 JSDOM() 构造器选项、fromFile/fromURL 加载方式这类 jsdom 专属 API,才适合用 Mocha 在 test/api/ 中编写。
为什么叫 "to-upstream"?
test/web-platform-tests/to-upstream/ 的名字源于它的使命:当官方 WPT 仓库(web-platform-tests/wpt)中没有覆盖你正在修复的 bug 时,jsdom 仍以 WPT 格式编写测试放在这里,希望日后把这些测试**反向提交(upstream)**到 WPT 官方仓库,让所有浏览器共同受益。存放时需在 to-upstream/ 下新建与 WPT 官方仓库结构平行的子目录(如 FileAPI/blob/、css/cssom/、dom/nodes/)。
环境准备与全量运行
首次运行前的系统配置
按照 Contributing.md 的说明,运行测试前需要两步准备:
npm install安装全部依赖(当前项目版本为 30.0.1,依赖配置见 package.json);- 按 WPT 官方文档配置本地系统环境,关键点是 hosts 文件必须包含
web-platform.test等 WPT 专用域名——wpt-server.js启动时会先执行dns.lookup("web-platform.test")进行探测,若解析失败会直接抛出错误并提示参考 WPT 的系统配置文档。
运行命令速查表
以下是本项目各测试子集的命令汇总(均来自 package.json 的 scripts 字段与 Contributing.md):
| 目标 | 命令 |
|---|---|
| 全量测试(含所有子集) | npm test |
| 运行子集前的必要准备 | npm run pretest |
| 全部 WPT(由 to-run.yaml 控制) | npm run test:wpt |
| 指定 WPT 目录(按名称过滤) | npm run test:wpt -- --fgrep dom/events |
| 全部 to-upstream WPT | npm run test:tuwpt |
| 指定 to-upstream WPT | npm run test:tuwpt -- --fgrep domparsing |
| 全部 jsdom API 测试 | npm run test:api |
| 单个 API 测试文件 | npx mocha test/api/from-file.js |
| 更新 WPT 到最新修订 | npm run wpt:update(耗时较长,可能约 10 分钟) |
几点说明:
npm test与子集的关系:npm test执行npm-run-all --continue-on-error test:*,会依次跑test:api、test:to-port-to-wpts、test:tuwpt、test:wpt全部子集。如果你只运行某个子集(而不是npm test),请先执行npm run pretest,它负责npm run prepare(构建 IDL 生成的公共 API)与npm run wpt:init(初始化 WPT 子模块)。--fgrep过滤:透传给底层 Mocha,按测试描述(describe 名)做子串匹配,例如--fgrep dom/events只运行 DOM 事件相关用例。wpt:update的实现:见 package.json,它先同步 WPT 子模块(git submodule update --init --recursive --remote),再在test/web-platform-tests/tests下调用python wpt.py manifest重新生成wpt-manifest.json。
深入 web-platform-tests 运行机制
清单、配置文件与三份关键文件
WPT 子套件由 run-wpts.js 驱动,其运行链路可以拆解为:
- 读取
wpt-manifest.json获得全部可用测试文件路径(清单由wpt.py manifest生成,工具函数在wpt-manifest-utils.js); - 用
expectations-utils.js的checkToRunFile()解析to-run.yaml,按目录分组得到“要运行哪些测试、各自预期结果是什么”; - 启动本地 WPT 服务器(
wpt-server.js通过python ./wpt.py serve --config ...拉起,并轮询 HTTP/HTTPS/WS/WSS 四个端口就绪); - 逐个运行测试文件并核对预期。
理解 to-run.yaml 的配置语法
to-run.yaml 是控制“jsdom 跑 WPT 官方仓库里哪些测试”的开关,其结构非常清晰:
DIR: FileAPI
Blob-methods-from-detached-frame.html: [timeout, Unknown]
blob/Blob-constructor.any.html:
"Blob interface object": [fail, function is not instanceof Function]
"Passing a Float16Array as element of the blobParts array should work.": [fail-node:<24, Float16Array not implemented]
DIR: <路径>:启用某个目录下的全部测试,路径对应 WPT 官方仓库中的目录(如css/cssom、FileAPI)。相邻两个DIR:之间用---分隔;- 目录下紧跟的条目:对单个测试文件或子测试设置“预期”。形如
[原因, 说明],第一项是预期类型,第二项是人类可读的原因注释; - glob 与批量匹配:支持
**、*通配(如BlobURL/**: [fail-slow, Unknown]),也支持!(a|b)排除语法(如animations/!(border-bottom-width-composition|...).html),还支持"old-tests/css3-modsel-{113..114}.xml"这样的花括号区间展开; - 文件级 vs 子测试级:条目值为数组时表示整个文件的预期;条目值为对象时,键是 testharness 子测试名,表示文件内部分子测试的预期;
- 硬性格式约束:
expectations-utils.js会做校验——DIR路径不允许以/开头或结尾、目录必须真实存在于子模块中、期望模式必须按字典序排列、fail/fail-slow/timeout等必须是合法取值,否则直接抛错。
预期值(expectation)的完整枚举
expectations-utils.js 中定义了完整的合法预期类型:
| 预期值 | 含义 |
|---|---|
fail |
预期失败(运行并断言失败成立) |
fail-slow |
预期失败且运行缓慢(直接跳过) |
timeout |
预期超时(跳过) |
flaky |
不稳定测试(跳过) |
needs-canvas |
需要 canvas 依赖(未安装则跳过) |
fail-with-canvas |
安装了 canvas 时预期失败(未安装则跳过) |
pass-slow |
通过但缓慢(跳过) |
fail-node:<semver范围> |
在指定 Node 版本范围内预期失败(如 fail-node:<24 表示 Node 24 以下) |
skip-node:<semver范围> |
在指定 Node 版本范围内跳过(仅文件级可用) |
结合 expectations-utils.js 的 resolveReason() 可以看到运行时的裁决逻辑:fail-slow、pass-slow、timeout、flaky 一律转为 skip;needs-canvas 与 fail-with-canvas 取决于本机是否加载了 canvas(utils.js 导出 Canvas,未安装则为空);fail-node: 前缀则用 semver 与当前 process.versions.node 比对。
新增/启用一个 WPT 用例的标准流程
假设你在 WPT 官方仓库中找到了覆盖某个特性的测试文件,Contributing.md 给出的流程是:
- 在
to-run.yaml中找到覆盖该目录的DIR:段; - 删除(或修改)其中禁用该测试的期望行;
- 若没有对应
DIR:段,则新增一个段并设置正确的预期。
to-upstream:本地 WPT 用例与真实浏览器验证
运行机制
run-tuwpts.js 与 WPT 主套件略有不同:由于 to-upstream/ 文件数量少,它每次运行都重新生成 manifest(而不是复用 wpt-manifest.json),并且会用 python ./wpt.py lint 对本地测试做格式检查,lint 失败则直接以非零状态退出。预期文件是 to-upstream-expectations.yaml,同样由 checkToUpstreamExpectations() 加载校验。
写了一个 jsdom 跑不通但浏览器能通过的测试怎么办
这是 WPT 贡献中常见的场景:Contributing.md 建议——仍然值得提交,把它放到 to-upstream/ 对应目录下,然后在 to-upstream-expectations.yaml 中为该测试添加一行“预期失败”,并附上记录进度的 issue 链接,供后续 jsdom 贡献者持续跟进。
用真实浏览器验证 to-upstream 测试
npm run test:tuwpt:browser
该命令(对应 scripts/verify-tuwpts-in-browser.js)会启动本地 WPT 服务器、打开默认浏览器、运行全部 to-upstream 测试并打印摘要。支持以下参数:
# 只跑 domparsing 相关,且只打印失败与摘要
npm run test:tuwpt:browser -- --fgrep domparsing --reporter min
# 用 Chromium 无头模式自动验证
npm run test:tuwpt:browser -- --browser=chromium --browser-arg=--headless --fgrep domparsing --reporter min
几个值得注意的行为细节:
--browser:指定 Chrome/Chromium 可执行文件实现全自动验证。运行器使用临时 profile(允许弹窗),测完自动关闭浏览器并删除 profile;每个测试在全新的顶层窗口中运行,且有独立控制器,即使测试发生导航或超时也能继续推进;--reporter min:仅打印失败与最终摘要;--browser-arg:可重复传入浏览器启动参数,例如无头模式--headless。运行器不会主动禁用浏览器沙箱,若环境需要请显式加--browser-arg=--no-sandbox;- 若显式传入
--browser-arg=--user-data-dir=...,则会覆盖临时 profile,此时运行器不会删除该目录; - 测试失败或浏览器启动出错都会导致非零退出状态。
Mocha API 测试的编写与运行
目录结构与定位
test/api/ 存放 jsdom API 的 Mocha 测试,从仓库现状看,它涵盖了 basics.js、options.js、options-run-scripts.js、cookies.js、encoding.js、fragment.js、from-file.js、from-url.js、from-outside.js、methods.js、resources*.js、virtual-console.js、jsdom-errors.js、css-parsing-errors.js、file-url-functionality.js 等主题,另有 helpers/(resources.js、servers.js)与 fixtures/(各类 HTML/XML/编码测试夹具)。这些文件分别对应 JSDOM 构造器的各种选项(如 resources、runScripts、url、cookieJar、virtualConsole、beforeParse 等)。
编写一个新 API 测试
Contributing.md 的指引是:在 test/api/ 下新增文件并遵循周边既有约定。需要注意,当前版本的 package.json 中 test:api 直接执行 mocha test/api/(按目录扫描),因此新增文件放入该目录即可被自动发现。
运行方式
# 全部 API 测试
npm run test:api
# 单个文件(以 from-file.js 为例)
npx mocha test/api/from-file.js
遗留的旧式测试:to-port-to-wpts
test/to-port-to-wpts/ 是历史遗留目录:其中的测试使用 Mocha 编写,但内容本应是 WPT 格式,jsdom 保留它们仅为覆盖率,直到被逐步转换。npm test 会通过 test:to-port-to-wpts(依次运行该目录及其 level1/level2/level3 子目录)把这些旧测试一并跑完——这正是 test/README.md 里“运行全量测试会包含旧测试”这一说法的来源。写新测试时不要往这里添加内容。
结语:一套服务于“与浏览器一致”的测试组合拳
从 test/README.md 出发可以看到,jsdom 测试体系的组织哲学非常一致:能测 Web 平台行为的,一律以 WPT 格式编写并尽量回馈官方仓库;只有 jsdom 专属 API 才用 Mocha 在 test/api/ 中测试。理解 to-run.yaml 与 to-upstream-expectations.yaml 的预期语法,就能读懂“哪些用例已启用、哪些已知失败及其原因”,这也是判断一个功能实现状态的最直接窗口。写测试前,建议同时阅读 Contributing.md 中关于 WPT 编写、to-run 配置与浏览器验证的完整说明,以及 test/web-platform-tests/expectations-utils.js 中预期值的权威定义,确保你的新用例既符合规范,也能被这套双框架体系正确接纳与长期维护。