Vitest 值格式化器深度解析:@vitest/pretty-format 如何驱动快照与断言输出
【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest
@vitest/pretty-format是 Vitest 对 Jest 官方pretty-format包的 ESM 化 fork,负责把任意 JavaScript 值渲染成人类可读的字符串。它并不直接面向测试编写者,而是支撑着 Vitest 的四条关键输出链路:快照序列化、断言 diff 渲染、匹配器与错误信息、以及浏览器模式的prettyDOM。读完全文,你将理解format的完整选项语义、内置插件体系、Vitest 在 fork 上新增的printShadowRoot与maxOutputLength机制,以及快照、diff、stringify各场景下实际使用的预设差异,从而能准确解释和控制测试输出中的每一个字符。
包的定位与发布形态
从 package.json 可以确认几个关键事实:
- 包名
@vitest/pretty-format,描述为 "Fork of pretty-format with support for ESM",即 Jestpretty-format的 ESM 支持版本; "type": "module"且产物只有dist/index.js,这是一个 ESM-only 包;- 运行时仅依赖
tinyrainbow(终端着色),react-is仅作为开发依赖; - 源码文件头部保留了 Meta Platforms 的 MIT 版权声明,例如 src/index.ts,说明它是在 Jest 许可基础上维护的衍生实现。
包的核心导出集中在 src/index.ts:一个format函数、一个plugins对象(内置插件集合)、一个createDOMElementFilter工具,以及一组类型。文档入口在 USAGE.md。
基础用法:format 函数
format(value, options?)接收任意值和可选配置,返回展示字符串:
import { format } from '@vitest/pretty-format' const value = { user: 'Ada', items: [1, 2, 3], } console.log(format(value)) /* -- output -- Object { "items": Array [ 1, 2, 3, ], "user": "Ada", } */注意默认输出会带Object、Array前缀并给对象键加双引号——这是printBasicPrototype: true和quoteKeys: true的默认行为,与console.log的 JSON 化输出不同。
完整配置选项
format接受以下选项(完整表见 USAGE.md,默认值定义在 DEFAULT_OPTIONS):
| key | type | default | 说明 |
|---|---|---|---|
callToJSON | boolean | true | 若存在toJSON则调用它进行序列化 |
compareKeys | function\|null | undefined | 对象键排序的自定义比较函数;传null表示按原始顺序不排序 |
escapeRegex | boolean | false | 转义正则中的特殊字符 |
escapeString | boolean | true | 转义字符串中的特殊字符 |
highlight | boolean | false | 使用终端颜色进行语法高亮 |
indent | number | 2 | 每级缩进的空格数 |
maxDepth | number | Infinity | 最大打印深度 |
maxOutputLength | number | 1_000_000 | 每层深度的近似输出预算(Vitest 扩展项,见下文) |
maxWidth | number | Infinity | 集合(数组/Map/Set/对象)中最多打印的条目数 |
min | boolean | false | 最小化额外空白 |
plugins | array | [] | 用于序列化应用专属数据类型的插件列表 |
printBasicPrototype | boolean | true | 为普通对象和数组打印Object/Array前缀 |
printFunctionName | boolean | true | 是否包含函数名 |
printShadowRoot | boolean | true | 序列化 DOM 节点时是否包含 shadow-root 内容(Vitest 扩展项) |
quoteKeys | boolean | true | 始终给对象属性键加引号 |
singleQuote | boolean | false | 字符串使用单引号而非双引号 |
spacingInner | string | \n | 逗号分隔项或条目之后的空白 |
spacingOuter | string | \n | []/{}定界符内侧紧邻的空白 |
有两条语义需要特别留意(源码依据见 getConfig 与 validateOptions):
plugins: []的默认值意味着包不会自动启用任何内置插件。你必须显式传入plugins才会走插件序列化路径;不传时,DOM、React、Immutable 等值会落到通用的复杂对象打印逻辑上。Vitest 各功能(快照、diff 等)在各自内部选定了自己的插件栈与预设,见下文。min: true会联动改写其他默认值:spacingInner变为' '、spacingOuter变为''、printBasicPrototype变为false,indent被禁用(若显式传入非 0 的indent会直接抛错)。- 传入未知选项键会抛出
pretty-format: Unknown option "...",这是防止拼写错误静默失效的硬校验。
内置插件体系
format的扩展点在于插件:每个插件有test(value)判定是否认领该值,serialize负责输出。findPlugin按数组顺序遍历,第一个test通过的插件生效(findPlugin 实现)。插件抛出的错误会被包装成PrettyFormatPluginError,序列化结果若非字符串则抛TypeError(printPlugin)。
包导出七个内置插件(导出见 index.ts 底部):
ReactTestComponent/ReactElement:React 测试容器与元素;DOMElement:HTML/SVG 元素、文本、注释、DocumentFragment;DOMCollection:DOM 节点集合(NodeList 等);Immutable:immutable-js 的 Map、Set、List、Stack、Seq、Record;AsymmetricMatcher:expect.anything()这类非对称匹配器;Error:Error 及子类,展开message、cause,AggregateError会额外打印errors(ErrorPlugin)。
直接使用示例:
import { format, plugins } from '@vitest/pretty-format' console.log( format(document.body, { plugins: [plugins.DOMElement, plugins.DOMCollection], }), )从源码看各插件的处理细节:
DOMElement(src/plugins/DOMElement.ts)按nodeType识别元素/文本/注释/Fragment,对自定义元素(标签含-或有is属性)也走 DOM 序列化;无filterNode时保留全部子节点,且默认过滤纯空白文本节点以避免空行(filterChildren)。Immutable(src/plugins/Immutable.ts)通过@@__IMMUTABLE_*__@@哨兵符号识别 immutable v3/v4 类型,输出形如Immutable.Map {...},惰性Seq会用…标记。- 集合打印统一走 src/collections.ts:
maxWidth超出时输出…(剩余数量)截断(如 printListItems);对象键排序由compareKeys控制,null时保持Object.keys原始顺序并追加可枚举 Symbol 键(getKeysOfEnumerableProperties);quoteKeys: false时符合/^[a-z_]\w*$/i的键不加引号(isUnquotableKey)。
打印管线:plugin → basic → complex
format的内部流程(printer)分三层:
- 插件层:
findPlugin命中则交给插件; - 基本类型层(printBasicValue):处理
null/undefined/布尔/数字/BigInt(输出123n)/字符串(按singleQuote、escapeString转义)/函数([Function name])/Symbol/Date(ISO 字符串或Date { NaN })/Error/RegExp(escapeRegex开启时转义元字符);-0会专门打印为-0;WeakMap/WeakSet打印为叶子; - 复杂对象层(printComplexValue):循环引用检测(
refs命中则返回[Circular])、callToJSON调用、Arguments/TypedArray/Map/Set分别处理,其余对象按构造器名输出TypeName { ... },并特判了 jsdom 环境下的全局window避免序列化失败。
Vitest 扩展:printShadowRoot 与 maxOutputLength
printShadowRoot
控制 DOM 序列化是否包含 shadow-root 内容,Vitest 在此 fork 中新增该选项:
format(element, { printShadowRoot: false, })对应实现位于 DOMElement 插件:序列化元素时同时收集node.shadowRoot.children,默认(printShadowRoot: true)一并输出,关闭则只打印 light DOM。
maxOutputLength
这是一个启发式安全阀,用于防止大型递归结构病态膨胀——它不是最终字符串长度的硬上限:
format(value, { maxOutputLength: 100_000, })从源码看其实现相当精巧(printer 尾部):配置对象内维护一个按深度分桶的计数器_outputLengthPerDepth[depth],每个深度层独立累计该层产生的输出长度;一旦某层累计超过maxOutputLength,直接把config.maxDepth置为0,让后续所有节点瞬间触顶、以[构造器名]叶子形式收尾。按深度分桶的原因写在注释里:嵌套结果若挤在同一个计数器上会因 N 层嵌套而低估约 N 倍,而同一深度的节点在输出串中互不重叠,分桶统计才是精确的。默认值1_000_000的注释也说明了取值动机——避免为日志和错误信息生成过长字符串(Node 的字符串上限约 512MB)。
Vitest 自身如何使用它
快照(Snapshot)
快照序列化使用快照专用预设(见 USAGE.md 与 docs/config/snapshotformat.md):
printBasicPrototype: falseescapeString: falseescapeRegex: trueprintFunctionName: falsemaxOutputLength: 2 ** 27
快照的maxOutputLength比包默认值1_000_000宽得多:包默认值面向日志和错误信息这类通用场景,而快照用户可能有意把很大的序列化值持久化到专用文件。若仍想收紧,可通过test.snapshotFormat.maxOutputLength配置。
默认快照插件栈为:ReactTestComponent、ReactElement、DOMElement、DOMCollection、Immutable、AsymmetricMatcher、MockSerializer(后者的MockFunction序列化由 Vitest 侧注册)。快照格式经由test.snapshotFormat配置,自定义序列化器则通过expect.addSnapshotSerializer或snapshotSerializers注册。
断言 Diff
断言 diff 使用不同的预设与插件栈。默认 diff 插件为:
ReactTestComponentReactElementDOMElementDOMCollectionImmutableAsymmetricMatcherError(比快照多出的关键项——diff 中错误对象会被展开为结构化的Error { message, cause, ... })
内部 stringify:匹配器与错误信息
匹配器和错误消息普遍经过 packages/utils/src/display.ts 中的stringify工具。其默认插件栈为 PLUGINS:
const PLUGINS = [ ReactTestComponent, ReactElement, DOMElement, DOMCollection, Immutable, AsymmetricMatcher, ]stringify(实现)在@vitest/pretty-format之上叠加了三层包装行为:
maxLength自适应降级:默认预算 10000 字符,若结果超长且maxDepth > 1,则以maxDepth / 2递归重试,逐级压缩深度直至输出可控(重试逻辑);filterNode:配置后会把默认DOMElement插件替换为经createDOMElementFilter包装的过滤变体,支持传 CSS 选择器字符串自动构造过滤器(过滤注释节点和匹配选择器的元素,createNodeFilterFromSelector);- 格式化失败兜底:若格式化抛错,自动以
callToJSON: false重试,规避toJSON实现中的副作用异常(catch 分支)。
同文件的inspect进一步封装了min、singleQuote、quoteKeys: false、compareKeys: null等紧凑输出预设,并带有truncate截断能力:优先靠stringify的maxDepth自适应压缩,仍不够时按类型做尽力截断(字符串直接切片,数组/对象/Map/Set 用二分搜索找到满足阈值的最小maxWidth,其他类型退回maxDepth: 0的最简输出)。
浏览器 prettyDOM
浏览器模式的prettyDOM构建在stringify路径之上,并启用面向浏览器的默认值,典型如highlight: true(终端/ANSI 颜色高亮);配置filterNode时同样替换为过滤版 DOM 插件。
小结
@vitest/pretty-format的设计核心是「一个纯函数式格式化内核 + 可插拔类型序列化器 + 每个消费方自选预设」:
- 内核(
format)负责基础类型、循环引用、深度/宽度截断、键排序与输出预算; - 七组内置插件覆盖 DOM、React、Immutable、Error、非对称匹配器等测试场景高频值类型,默认不自动启用;
- Vitest 的快照、diff、
stringify、浏览器prettyDOM四条链路各自组装插件栈与选项预设,形成差异化的输出风格; - fork 新增的
printShadowRoot和按深度分桶的maxOutputLength安全阀,是对 Web 平台与大对象场景的针对性补强。
如果你需要控制测试输出形态(比如快照中不出现Object前缀、错误信息里截断 DOM 子树),入口分别是 docs/config/snapshotformat.md 中的snapshotFormat配置和 docs/api/expect.md 中的expect.addSnapshotSerializer,它们的最终落点都是本文所述的format(value, options)调用链。
【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考