Skip to content
72 changes: 72 additions & 0 deletions docs/api/browser/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,75 @@ export const utils: {
getElementError(selector: string, container?: Element): Error
}
```

### configurePrettyDOM
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move the version tag to the head, like in every other page in the documentation. add a custom {#configureprettydom} anchor

do the same for Filtering with CSS Selectors section

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved version tags to heading line and added custom anchors in commit 15d1697:

  • ### configurePrettyDOM <Version>4.1.0</Version> {#configureprettydom}
  • #### Filtering with CSS Selectors <Version>4.1.0</Version> {#filtering-with-css-selectors}


<Version>4.1.0</Version>

The `configurePrettyDOM` function allows you to configure default options for the `prettyDOM` and `debug` functions. This is useful for customizing how HTML is formatted in test failure messages.

```ts
import { utils } from 'vitest/browser'

utils.configurePrettyDOM({
maxDepth: 3,
filterNode: 'script, style, [data-test-hide]'
})
```

#### Options

- **`maxDepth`** - Maximum depth to print nested elements (default: `Infinity`)
- **`maxLength`** - Maximum length of the output string (default: `7000`)
- **`filterNode`** - A CSS selector string or function to filter out nodes from the output. When a string is provided, elements matching the selector will be excluded. When a function is provided, it should return `false` to exclude a node.
- **`highlight`** - Enable syntax highlighting (default: `true`)
- And other options from [`pretty-format`](https://www.npmjs.com/package/@vitest/pretty-format)

#### Filtering with CSS Selectors

<Version>4.1.0</Version>

The `filterNode` option allows you to hide irrelevant markup (like scripts, styles, or hidden elements) from test failure messages, making it easier to identify the actual cause of failures.

```ts
import { utils } from 'vitest/browser'

// Filter out common noise elements
utils.configurePrettyDOM({
filterNode: 'script, style, [data-test-hide]'
})

// Or use directly with prettyDOM
const html = utils.prettyDOM(element, undefined, {
filterNode: 'script, style'
})
```

**Common Patterns:**

Filter out scripts and styles:
```ts
utils.configurePrettyDOM({ filterNode: 'script, style' })
```

Hide specific elements with data attributes:
```ts
utils.configurePrettyDOM({ filterNode: '[data-test-hide]' })
```

Hide nested content within an element:
```ts
// Hides all children of elements with data-test-hide-content
utils.configurePrettyDOM({ filterNode: '[data-test-hide-content] *' })
```

Combine multiple selectors:
```ts
utils.configurePrettyDOM({
filterNode: 'script, style, [data-test-hide], svg'
})
```

::: tip
This feature is inspired by Testing Library's [`defaultIgnore`](https://testing-library.com/docs/dom-testing-library/api-configuration/#defaultignore) configuration.
:::
32 changes: 17 additions & 15 deletions packages/pretty-format/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,7 @@ import Immutable from './plugins/Immutable'
import ReactElement from './plugins/ReactElement'
import ReactTestComponent from './plugins/ReactTestComponent'

export type {
Colors,
CompareKeys,
Config,
NewPlugin,
OldPlugin,
Options,
OptionsReceived,
Plugin,
Plugins,
PrettyFormatOptions,
Printer,
Refs,
Theme,
} from './types'
export { createDOMElementFilter } from './plugins/DOMElementFilter'

const toString = Object.prototype.toString
const toISOString = Date.prototype.toISOString
Expand Down Expand Up @@ -559,6 +545,22 @@ export function format(val: unknown, options?: OptionsReceived): string {
return printComplexValue(val, getConfig(options), '', 0, [])
}

export type {
Colors,
CompareKeys,
Config,
NewPlugin,
OldPlugin,
Options,
OptionsReceived,
Plugin,
Plugins,
PrettyFormatOptions,
Printer,
Refs,
Theme,
} from './types'

export const plugins: {
AsymmetricMatcher: NewPlugin
DOMCollection: NewPlugin
Expand Down
153 changes: 153 additions & 0 deletions packages/pretty-format/src/plugins/DOMElementFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type { Config, NewPlugin, Printer, Refs } from '../types'
import {
printChildren,
printComment,
printElement,
printElementAsLeaf,
printProps,
printShadowRoot,
printText,
} from './lib/markup'

const ELEMENT_NODE = 1
const TEXT_NODE = 3
const COMMENT_NODE = 8
const FRAGMENT_NODE = 11

const ELEMENT_REGEXP = /^(?:(?:HTML|SVG)\w*)?Element$/

function testHasAttribute(val: any) {
try {
return typeof val.hasAttribute === 'function' && val.hasAttribute('is')
}
catch {
return false
}
}

function testNode(val: any) {
const constructorName = val.constructor.name
const { nodeType, tagName } = val
const isCustomElement
= (typeof tagName === 'string' && tagName.includes('-'))
|| testHasAttribute(val)

return (
(nodeType === ELEMENT_NODE
&& (ELEMENT_REGEXP.test(constructorName) || isCustomElement))
|| (nodeType === TEXT_NODE && constructorName === 'Text')
|| (nodeType === COMMENT_NODE && constructorName === 'Comment')
|| (nodeType === FRAGMENT_NODE && constructorName === 'DocumentFragment')
)
}

export const test: NewPlugin['test'] = (val: any) =>
val?.constructor?.name && testNode(val)

type HandledType = Element | Text | Comment | DocumentFragment

function nodeIsText(node: HandledType): node is Text {
return node.nodeType === TEXT_NODE
}

function nodeIsComment(node: HandledType): node is Comment {
return node.nodeType === COMMENT_NODE
}

function nodeIsFragment(node: HandledType): node is DocumentFragment {
return node.nodeType === FRAGMENT_NODE
}

export interface FilterConfig extends Config {
filterNode?: (node: any) => boolean
}

function filterChildren(children: any[], filterNode?: (node: any) => boolean): any[] {
if (!filterNode) {
return children
}
return children.filter(filterNode)
}

export function createDOMElementFilter(filterNode?: (node: any) => boolean): NewPlugin {
return {
test,
serialize: (
node: HandledType,
config: Config,
indentation: string,
depth: number,
refs: Refs,
printer: Printer,
) => {
if (nodeIsText(node)) {
return printText(node.data, config)
}

if (nodeIsComment(node)) {
return printComment(node.data, config)
}

const type = nodeIsFragment(node)
? 'DocumentFragment'
: node.tagName.toLowerCase()

if (++depth > config.maxDepth) {
return printElementAsLeaf(type, config)
}

const children = Array.prototype.slice.call(node.childNodes || node.children)
const filteredChildren = filterChildren(children, filterNode)

const shadowChildren = (nodeIsFragment(node) || !node.shadowRoot)
? []
: Array.prototype.slice.call(node.shadowRoot.children)
const filteredShadowChildren = filterChildren(shadowChildren, filterNode)

return printElement(
type,
printProps(
nodeIsFragment(node)
? []
: Array.from(node.attributes, attr => attr.name).sort(),
nodeIsFragment(node)
? {}
: [...node.attributes].reduce<Record<string, string>>(
(props, attribute) => {
props[attribute.name] = attribute.value
return props
},
{},
),
config,
indentation + config.indent,
depth,
refs,
printer,
),
(filteredShadowChildren.length > 0
? printShadowRoot(filteredShadowChildren, config, indentation + config.indent, depth, refs, printer)
: '')
+ printChildren(
filteredChildren,
config,
indentation + config.indent,
depth,
refs,
printer,
),
config,
indentation,
)
},
}
}

export default createDOMElementFilter
50 changes: 46 additions & 4 deletions packages/utils/src/display.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { PrettyFormatOptions } from '@vitest/pretty-format'
import {
createDOMElementFilter,
format as prettyFormat,
plugins as prettyFormatPlugins,
} from '@vitest/pretty-format'
Expand Down Expand Up @@ -42,22 +43,39 @@ const PLUGINS = [

export interface StringifyOptions extends PrettyFormatOptions {
maxLength?: number
filterNode?: string | ((node: any) => boolean)
}

export function stringify(
object: unknown,
maxDepth = 10,
{ maxLength, ...options }: StringifyOptions = {},
{ maxLength, filterNode, ...options }: StringifyOptions = {},
): string {
const MAX_LENGTH = maxLength ?? 10000
let result

// Convert string selector to filter function
const filterFn = typeof filterNode === 'string'
? createNodeFilterFromSelector(filterNode)
: filterNode

const plugins = filterFn
? [
ReactTestComponent,
ReactElement,
createDOMElementFilter(filterFn),
DOMCollection,
Immutable,
AsymmetricMatcher,
]
: PLUGINS

try {
result = prettyFormat(object, {
maxDepth,
escapeString: false,
// min: true,
plugins: PLUGINS,
plugins,
...options,
})
}
Expand All @@ -67,17 +85,41 @@ export function stringify(
maxDepth,
escapeString: false,
// min: true,
plugins: PLUGINS,
plugins,
...options,
})
}

// Prevents infinite loop https://github.com/vitest-dev/vitest/issues/7249
return result.length >= MAX_LENGTH && maxDepth > 1
? stringify(object, Math.floor(Math.min(maxDepth, Number.MAX_SAFE_INTEGER) / 2), { maxLength, ...options })
? stringify(object, Math.floor(Math.min(maxDepth, Number.MAX_SAFE_INTEGER) / 2), { maxLength, filterNode, ...options })
: result
}

function createNodeFilterFromSelector(selector: string): (node: any) => boolean {
const ELEMENT_NODE = 1
const COMMENT_NODE = 8

return (node: any) => {
// Filter out comments
if (node.nodeType === COMMENT_NODE) {
return false
}

// Filter out elements matching the selector
if (node.nodeType === ELEMENT_NODE && node.matches) {
try {
return !node.matches(selector)
}
catch {
return true
}
}

return true
}
}

export const formatRegExp: RegExp = /%[sdjifoOc%]/g

interface FormatOptions {
Expand Down
Loading