@ctrl/dom

中文文档
0.2.0发布于 2026/11/10MITTypeScriptESM + CJS构建来源已验证近 30 天下载 3209

用于测试 React 组件的快取 DOM : jsdom 和 happy-dom 的倒置替换, 包含测试库, 用户事件和快取dom 的构建.

中文使用文档
文档目录16 节
  1. 维特
  2. 欢乐
  3. 其他试跑者
  4. 节点:测试
  5. 阿宝
  6. 配置
  7. 故事书
  8. 时间跳过
  9. 直接进口
  10. 选项
  11. 共有工人
  12. 兼容配置文件
  13. 用编码代理调试
  14. 哨兵
  15. 与jsdom的区别
  16. 许可证

@ ctrl/ dom

用于测试 React 组件的 DOM. 它取代了Jsdom或快乐的地盘, 并进行自己的测试 图书馆,用户活动 和笑柄建在它。 一个100个文件的回放套房 以3.6s的速度运行 与14.4 s在jsdom和9.3 s在快乐度, 相比,记忆力更弱.

  • 滴入来. 与 jsdom 同为全局, 测试库执行 导出、行为、错误消息和 prettyDOM 输出为 @testing-library/dom, (中文(简体) ), /react, (中文(简体) ), /user-event 财务报告和审定财务报表 /jest-dom只交换DOM,或者整个堆栈,没有触摸测试.
  • 快点. 查询类 getByRole 取微秒而不是毫秒, 用户事件 不等待计时器 在键盘中,和闲置的时间在 findBy*, (中文(简体) ), waitFor 应用 在虚拟时钟上跳出跳出而不是等待出场(关掉) (中文(简体) ).
  • 为组件测试所建. 没有布局、脚本执行、导航、网络; react 组件触碰的所有其他东西都像浏览器一样,或者像测试时的jsdom 套房取决于它.
支助
节点26岁以上
测试运行器Vest 5+, Jest 30+, 节点: test, Bun
反应19(续)@ctrl/dom/react 用途 React.act(中文(简体) )

节点: test and Bun: 其他试跑者。 。 。 .

维特

整个堆栈:环境加上您现有的 @testing-library/* 进口到 @ctrl/dom。 。 。 .

pnpm add -D @ctrl/dom
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { ctrlDom } from "@ctrl/dom/vitest";

export default defineConfig({
  plugins: [ctrlDom()],
  // Optional and faster: each test file gets a fresh context in a reused worker instead of a
  // new worker, so per-file setup drops from about 23 ms to about 2 ms.
  test: { pool: "vmThreads" },
});

DOM( 库存测试库软件包继续工作 ) :

export default defineConfig({ plugins: [ctrlDom({ testingLibrary: false })] });

选项转到插件 : ctrlDom({ url: "http://localhost:3000/", width: 1280 })与 安装了插件, 文件也可以选择环境本身 : // @vitest-environment ctrl-dom。 。 。 . 一个 environment 您在配置中设置了其它 "ctrl-dom" 保留项目的默认值.

插件还为Vitest 的员工添加了节点旗, 因为默认池开始每次测试一次 文件:随着它的测试库,它会随着每个工人的开始打开编译缓存,所以Vitest的 不仅包括DOM系统.

欢乐

pnpm add -D @ctrl/dom @ctrl/dom-jest-environment
// jest.config.js
module.exports = {
  preset: "@ctrl/dom", // the environment + @testing-library/* routed to @ctrl/dom
  setupFilesAfterEnv: ["@ctrl/dom/jest-dom"], // instead of "@testing-library/jest-dom"
};

仅限DOM: testEnvironment: "@ctrl/dom-jest-environment" (股票测试库继续工作). 选项进入 testEnvironmentOptions。每个文件 : /** @jest-environment @ctrl/dom-jest-environment */。 。 。 .

其他试跑者

@ctrl/dom/register 为没有 DOM 环境的跑者安装一个窗口作为全局, 如 快乐之地 @happy-dom/global-registrator,随着Vitest环境的默认值: jsdom-30 简介, http://localhost:3000/,空闲时间在虚拟时钟上跳过,以及例外 被未通过运行测试的事件听众所抛出 。 测试库的自动 cleanup() 之后 下方的两只跑道,每只试验 @testing-library/react 包含.

节点:测试

node --import @ctrl/dom/register --test

或者说 import "@ctrl/dom/register"; 作为测试文件的第一次导入。 节点:测试的钩子不是 环球,所以它暴露为 beforeAll, (中文(简体) ), afterAll, (中文(简体) ), beforeEach 财务报告和审定财务报表 afterEach (只有名称) jest 和 Bun 的方法: 测试库正在寻找。 之后 最后测试它关闭了窗口并放下了等待的定时器,所以被遗忘了 setInterval 没有 保持文件进程运行 。 文件自节点起每个窗口都获得新窗口: 测试运行每个文件 其自己的进程 --test-isolation=none 他们分享一个.

笑柄匹配者需要一个 expect. 杰斯特的独立 expect 把它们当作是 node:assert,对 DOM 属性进行断言:

// signup.test.ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { expect } from "expect";
import * as matchers from "@ctrl/dom/jest-dom/matchers";
import { createElement } from "react";
import { render, screen } from "@ctrl/dom/react";
import userEvent from "@ctrl/dom/user-event";
import { Signup } from "./signup.ts";

expect.extend(matchers);

test("signs up", async () => {
  const user = userEvent.setup();
  render(createElement(Signup));
  assert.equal((screen.getByRole("button", { name: "Sign up" }) as HTMLButtonElement).disabled, true);
  await user.type(screen.getByLabelText("Name"), "Ada");
  expect(screen.getByRole("textbox", { name: "Name" })).toHaveValue("Ada");
});

节点条 类型脚本但不编译 JSX: 写入 .ts 文件为 createElement,或 添加加载器, 如 tx 页:1 .tsx 文件 (单位:千美元)node --import tsx --import @ctrl/dom/register --test,与 "jsx": "react-jsx" 在你身边 (原始内容存档于2018-09-26). tsconfig.json.

阿宝

# bunfig.toml
[test]
preload = ["@ctrl/dom/register"]
// signup.test.tsx
import { expect, test } from "bun:test";
import * as matchers from "@ctrl/dom/jest-dom/matchers";
import { render, screen } from "@ctrl/dom/react";
import userEvent from "@ctrl/dom/user-event";
import { Signup } from "./signup.tsx";

expect.extend(matchers);

test("signs up", async () => {
  const user = userEvent.setup();
  render(<Signup />);
  expect(screen.getByRole("button", { name: "Sign up" })).toBeDisabled();
  await user.type(screen.getByLabelText("Name"), "Ada");
  expect(screen.getByRole("textbox", { name: "Name" })).toHaveValue("Ada");
});

bun test 在一个全局中运行每个文件, 因此文件共享窗口( 如 Vitest 和 isolate: false(三); bun test --isolate (或 --parallel)给每个文件一个新的全局和 窗口。 Bun自己的测试钩已经是全球性的了.

配置

预装不需争辩,所以读作 CTRL_DOM_URL, (中文(简体) ), CTRL_DOM_COMPAT (单位:千美元)jsdom-26, (中文(简体) ), jsdom-30, (中文(简体) ), modern, (中文(简体) ), CTRL_DOM_TIME_SKIP (单位:千美元)0编号 :1和 CTRL_DOM_TEST_HOOKS=0 (让跑者独活). 其他一切都会通过API,它 @ctrl/dom/register/pure 未登记的出口 导入时 :

import { register, unregister } from "@ctrl/dom/register/pure";

const { window } = register({ url: "http://localhost:5173/", compat: "modern", width: 1280 });
// ...
unregister(); // closes the window and restores every global it replaced

register(options) 拿着 环境选项 (除外) compileCache 财务报告和审定财务报表 nodeFetch+) 加上 (中文(简体) ) testHooks (默认) false 在这里, true 页:1 @ctrl/dom/register页:1 任何以前的登记.

故事书

便携式故事 运行 在ctrl-dom Vitest环境中,在节点下:测试或装有 @ctrl/dom/register数字 : composeStories 故事渲染为 @ctrl/dom/react是因为 render,以及 await Story.run() 挂载 故事并运行它 play 函数( 故事本 storybook/test 带来自己的测试库 和用户活动,运行在同一DOM上).

import { composeStories } from "@storybook/react";
import { render, screen } from "@ctrl/dom/react";
import * as stories from "./Signup.stories.tsx";

const { Empty, Filled } = composeStories(stories);

test("renders", () => {
  render(<Empty />);
  expect(screen.getByRole("button", { name: "Sign up" })).toBeDisabled();
});

test("plays", async () => {
  await Filled.run();
  expect(screen.getByRole("status")).toHaveTextContent("Hi, Ada!");
});

run() 如jsdom:测试库 cleanup() 不删除, 因此同一文件中的稍后测试会看到两者( 重复) ids 断开标签 抬头看. 测试钩子是全局的( Jest, Bun, 节点: 有寄存器的测试), 导入 测试库的 React 条目将 React 的行为环境打开, React 然后警告 play 函数外部的状态更新 act(...); 测试仍然通过.

时间跳过

计时器运行在虚拟时钟上. 当测试无所事事时,只能等待(无待决) 承诺, setImmediate 回调或I/O,时钟跳到下个计时器,以及 Date, (中文(简体) ), performance.now() 事件时间戳随它而移动。 A 类 findBy* 等待800多米的弹出 以微秒计返回;计时器仍然按顺序起火,以及假计时器(vi.useFakeTimers(), (中文(简体) ), jest.useFakeTimers())接任如常.

测试需要实时通过时关闭:来自 node:timers,或从图书馆 抓住那个 setTimeout 在环境开始前, 播放页面的计时器; 或测试 对照过程外的时钟测量时间。 真正的I/O(口袋、文件、儿童流程) 已经持有时钟, 因此一个真正的服务器的空闲度 永远不会被跳过 .

范围怎么样
Vest, 每个文件ctrlDom({ timeSkip: false })
Vest, 一个文件// @vitest-environment-options {"timeSkip": false} 作为第一行
欢乐,每个文件testEnvironmentOptions: { timeSkip: false }
Jest, 一个文件/** @jest-environment-options {"timeSkip": false} */ 在顶端
节点:测试, Bunregister({ timeSkip: false }) 从 @ctrl/dom/register/pure
任何跑者,一个跑者CTRL_DOM_TIME_SKIP=0,它优先于选项

直接进口

改为导入
@testing-library/react@ctrl/dom/react (另见: /react/pure, (中文(简体) ), /react/dont-cleanup-after-each(中文(简体) )
@testing-library/dom@ctrl/dom/testing-library
@testing-library/user-event@ctrl/dom/user-event
@testing-library/jest-dom@ctrl/dom/jest-dom (另见: /jest-dom/vitest, (中文(简体) ), /jest-dom/matchers, (中文(简体) ), /jest-dom/jest-globals(中文(简体) )
aria-query + 键 dom-accessibility-api@ctrl/dom/aria
jsdom 编号 : happy-dom@ctrl/dom (单位:千美元)createWindow(), (中文(简体) ), installGlobals(window)(中文(简体) )
@happy-dom/global-registrator@ctrl/dom/register (单位:千美元)register(), (中文(简体) ), unregister() 从 /register/pure(中文(简体) )

在任何一种环境下, @ctrl/dom 每一个子路径共享环境的 DOM 实例, 和每个游泳池(包括 vmThreads编号 :vmForks),这样 instanceof 对照 @ctrl/dom班级都坚持住 窗户也一样 @ctrl/dom/register 安装.

选项

环境选项environmentOptions["ctrl-dom"] 在韦斯特, testEnvironmentOptions 内 贾斯特:

选项默认
urlhttp://localhost:3000/ (视频), http://localhost/ (贾斯特)文档 URL
width, (中文(简体) ), height1024×768 (中文(简体) )innerWidth 编号 : innerHeight驱动器 matchMedia
userAgent, (中文(简体) ), platform, (中文(简体) ), languages, (中文(简体) ), devicePixelRatio, (中文(简体) ), referrer类似 jsdom 的导航/窗口值
timeSkiptrue跳过虚拟时钟上的闲置时间; false 保留实时计时器. CTRL_DOM_TIME_SKIP=0/1 覆盖
compat"jsdom-30" (视频), "jsdom-26" (贾斯特)超过jsdom的 API 被曝光 :
layout关"lite": 几何学的大致框模型(getBoundingClientRect, (中文(简体) ), offset*, (中文(简体) ), client*, (中文(简体) ), scroll*, 调整观察员大小),因此虚拟化器(TanStack Virtual,反应-窗口)和定位库在没有手写模拟的情况下工作. 关闭,几何是零,就像jsdom。 细节 : 文档/推出
compileCache (声音)true 带有插件的测试库, 否则 false允许节点在导入 DOM( 插件: 每个工人开始时) 之前为每个工人编译缓存 。 它保留在工人负荷的所有东西上,这只支付大文件的费用(库存测试库的许多小文件的负荷较慢). CTRL_DOM_COMPILE_CACHE=0/1 覆盖
nodeFetch (贾斯特)false保持节点 fetch编号 :Request编号 :Response (在笑话-环境-jsdom下缺席)

仅Vitest 插件的选项( E)ctrlDom({ ... }):

选项默认
testingLibrarytrue路线储存 @testing-library/* 导入到 @ctrl/ dom 的执行; false 只交换 DOM
jsxRuntimetrue页:1 react/jsx-runtime 财务报告和审定财务报表 react/jsx-dev-runtime 给ES模块的复制件。 病毒手测试和应用代码 一个代理代号 相对于一个普通JS依赖, 所以每个JSX元素 否则会通过代理 get 陷阱;没有它,钩子重的渲染速度会快8%. 当项目解决运行时间到任何结果时, 独自一人
sharedWorkersfalse在同一领域逐一运行工人的测试文件,而不是启动每个文件的工人:见下文
debug关"aria" 或者说 "compact"数字 : 用编码代理调试

共有工人

export default defineConfig({ plugins: [ctrlDom({ sharedWorkers: true })] });

大部分很多短测试文件的成本 不是测试: 每个文件开始一个工人, 它的靴子节点, 装入维特,反应和反应度, 运行所有它冷. sharedWorkers 套装 维特的 isolate: false,所以一个工人 不停地从文档到文档, 并放回大多数 什么是 新生提供了每个文件。 基础UI的9,199测试 采用35 s隔离 和7 -8 这样 同样的结果.

每个测试文件前 :

  • 重新评价第一当事方模块(vi.resetModules()),因此模块状态和 vi.mock() 重新开始;
  • 窗口被重置: 仍然挂载的树被卸载, 等待计时器和动画框 抛出、空出、聚焦、存储、饼干和放回URL;
  • 测试库的清理登记为文件(库存测试库登记为 只有工人的第一个文件, 这就是为什么模式需要 testingLibrary) (中文(简体) ).

文件仍然能从早先的文件中看到什么,为什么模式是选入的 :

  • 模块依赖状态。 有毒负载 node_modules 通过节点,每个工人一次: React,一个CSS-in-JS缓存,一个依赖性中模块范围创建的数据牵引客户端;
  • <head>,事件听众打开 window 财务报告和审定财务报表 document,和补丁原型,这些 模块可依赖;
  • 记忆。 维特保持每一个测试,它运行 达到工人的生命(柴记 每个 expect.extend vitest每次测试一次),并以此为文件模块:a 工人的堆积由文件的模块图每个文件的大小所增长,2.3 MB在Base UI上. 再来 工人意味着每个文件更少.

自愿流动资金库pool: "vmThreads"已经重复使用每个文件有新上下文的工人,并且 不受影响.

兼容配置文件

用于 jsdom 特征检测的应用程序: 找到的库 PointerEvent, (中文(简体) ), matchMedia 或者说 ResizeObserver 取取其浏览器路径, 以及仅在 API 时安装自身的多填充 离开我们的地方 而不是它的模拟. compat 选择测试全球曝光的内容:

compat匹配Jsdom以外的展览 30
"jsdom-26"jest- environment- jsdom 30 (jsdom 26.1); 游戏默认值没有 PointerEvent 或者说 onpointer* 处理者
"jsdom-30"Vitest 的 jsdom 环境; Vitest 默认无库特性检测
"modern"所有@ctrl/ dom 执行matchMedia, (中文(简体) ), ResizeObserver,则 inert IDL属性, visualViewport

文件/索引 列出每个 API 中 每个剖面图,有原因.

用编码代理调试

screen.debug() 和失败的查询将整个 DOM 打印为 HTML, 费用为千位代理 的符号和SVG路径。 运行测试 CTRL_DOM_DEBUG=aria (或 ctrlDom({ debug: "aria" })在 Playwright 的 aria 快照中打印无障碍树 格式,另加最接近的匹配 *ByRole, (中文(简体) ), *ByText 或者说 *ByLabelText 查询 失败 :

Unable to find an accessible element with the role "button" and name "Submit"

Closest matches:
- text: "Submit"  # in <div>, which has no role: give it role="button" or use getByText
- button "Pay now"  # same role, other name

Accessibility snapshot of <body> (CTRL_DOM_DEBUG=aria):
- main:
  - heading "Checkout" [level=1]
  - form "Payment":
    - textbox "Email address"
    - button "Pay now"
    - text: Submit

该错误为468个字符;上游印出2,476个同为小字形,约437,000个 一个大页. ariaSnapshot(el?) 编号 : screen.ariaSnapshot(), (中文(简体) ), prettyDOM(el, max, { format: "aria" }) 财务报告和审定财务报表 diffAriaSnapshots(before, after) 直接提供. CTRL_DOM_DEBUG=compact 指纹 HTML没有类/类属性和SVG内部. 没有变量,输出是相同的 到测试图书馆.

在同一模式下, 每次测试失败 (失败) toBe,一个抛出的错误,而不仅仅是查询)结束 并附有一页的快照,当它失败时, 在清理之前, 加上自 测试的前一个快照。 杰斯特的环境是靠自己完成的 与Vitest, ctrlDom() 添加 设置文件,或添加 "@ctrl/dom/vitest-environment/failure-snapshot" 改为 setupFiles 与 裸露的环境.

npx ctrl-dom debug <test file> [-t name] 在一个命令中,它能发现维特和杰斯特, 以代理模式运行文件,并打印一个紧凑的报表(其快照和 最接近的匹配,一个堆放框,控制台输出,随测试状态退出.

为了说明快照的打印方式 滑稽的动作 expect(el).toMatchAriaSnapshot(template),与 剧作家 部分匹配 :

expect(screen.getByRole("form")).toMatchAriaSnapshot(`
  - textbox "Email"
  - button /Pay \\d+ €/ [disabled]
`);

给你的一条线 CLAUDE.md 编号 : AGENTS.md数字 :

当组件测试失败时, 请重新运行 npx ctrl-dom debug <file> [-t <name>] 阅读 无障碍快照和最接近的匹配,而不是添加 screen.debug()。 。 。 .

细节 : 文件/代理调试。 。 。 .

哨兵

@ctrl/sentry-tests 在哨兵中进行测试: 测试失败 与失败的行, 测试所做的面包屑和页面 附加的片状试验 FlakyTest 以及每个测试文件 作为其描述块的痕迹, 测试、钩子和测试功能。 在@ctrl/dom上,痕迹还包含每个测试库查询, waitFor、用户活动动作和渲染(findByRole("option", …) (40 checks)),失败则得到 页面的无障碍快照.

plugins: [ctrlDom(), sentryTests()]                // Vitest, sentryTests from @ctrl/sentry-tests/vitest

Vitest 和 Jest 的设置 : 其读物。 。 。 .

与jsdom的区别

没有脚本执行 没有导航 没有网络 像jsdom和Happy -dom一样 没有 除非打开布局 layout: "lite" (其他几何为零,如jsdom). 每个 故意差别在以下列出: 文件/索引。 。 。 .

许可证

麻省理工学院. 包括从测试库移植的代码,dom-access-api,aria-query, Playwright, jsdom及其他人;他们的版权通知和许可证在包裹中 NOTICE 文档.

版本与发布记录

@ctrl/dom 共发布 18 个版本,首次发布于 2026/10/26;过去 12 个月发布了 18 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.0latest2026/11/101 天前
0.1.152026/11/037 天前
0.1.142026/11/038 天前
0.1.132026/11/038 天前
0.1.122026/10/2913 天前
0.1.112026/10/2913 天前

关于 @ctrl/dom 的常见问题

@ctrl/dom 是什么?

用于测试 React 组件的快取 DOM : jsdom 和 happy-dom 的倒置替换, 包含测试库, 用户事件和快取dom 的构建.

如何安装 @ctrl/dom?

在项目目录运行 npm install @ctrl/dom,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 ctrl-dom,可以用 npx ctrl-dom 直接运行。

@ctrl/dom 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ctrl/dom 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ctrl/dom 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=26。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ctrl/dom 可以商用吗?

@ctrl/dom 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ctrl/dom 的下载量有多少?

根据 npm 官方下载统计,@ctrl/dom 在 2026/10/11 – 2026/11/09 这 30 天内被下载 3,209 次,日均约 107 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ctrl/dom 还在维护吗?

最新版本 0.2.0 发布于 2026/11/10(1 天前);至今共发布 18 个版本,其中过去 12 个月发布了 18 个;首次发布于 2026/10/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。