@ 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} */ 在顶端 |
| 节点:测试, Bun | register({ 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 内
贾斯特:
| 选项 | 默认 | |
|---|---|---|
url | http://localhost:3000/ (视频), http://localhost/ (贾斯特) | 文档 URL |
width, (中文(简体) ), height | 1024×768 (中文(简体) ) | innerWidth 编号 : innerHeight驱动器 matchMedia |
userAgent, (中文(简体) ), platform, (中文(简体) ), languages, (中文(简体) ), devicePixelRatio, (中文(简体) ), referrer | 类似 jsdom 的 | 导航/窗口值 |
timeSkip | true | 跳过虚拟时钟上的闲置时间; 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({ ... }):
| 选项 | 默认 | |
|---|---|---|
testingLibrary | true | 路线储存 @testing-library/* 导入到 @ctrl/ dom 的执行; false 只交换 DOM |
jsxRuntime | true | 页:1 react/jsx-runtime 财务报告和审定财务报表 react/jsx-dev-runtime 给ES模块的复制件。 病毒手测试和应用代码 一个代理代号 相对于一个普通JS依赖, 所以每个JSX元素 否则会通过代理 get 陷阱;没有它,钩子重的渲染速度会快8%. 当项目解决运行时间到任何结果时, 独自一人 |
sharedWorkers | false | 在同一领域逐一运行工人的测试文件,而不是启动每个文件的工人:见下文 |
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.extendvitest每次测试一次),并以此为文件模块: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 文档.