Skip to content
77 changes: 77 additions & 0 deletions docs/api/browser/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,86 @@ export const utils: {
* @experimental
*/
configurePrettyDOM(options: StringifyOptions): void
/**
* Creates a filter function for prettyDOM that filters out nodes based on CSS selectors.
* This is similar to Testing Library's defaultIgnore configuration.
* @experimental
*/
createNodeFilter(selector: string): (node: any) => boolean
/**
* Creates "Cannot find element" error. Useful for custom locators.
*/
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: utils.createNodeFilter('script, style')
})
```

#### Options

- **`maxDepth`** - Maximum depth to print nested elements (default: `Infinity`)
- **`maxLength`** - Maximum length of the output string (default: `7000`)
- **`filterNode`** - A function to filter out nodes from the output. 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)

### createNodeFilter

<Version>4.1.0</Version>

The `createNodeFilter` function creates a filter function that can be used with `configurePrettyDOM` or passed directly to `prettyDOM`/`debug`. It filters out DOM nodes based on CSS selectors.

This is particularly useful for hiding 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
const filterNode = utils.createNodeFilter('script, style, [data-test-hide]')

// Use with configurePrettyDOM (affects all debug/prettyDOM calls)
utils.configurePrettyDOM({ filterNode })

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

#### Common Patterns

**Filter out scripts and styles:**
```ts
utils.createNodeFilter('script, style')
```

**Hide specific elements with data attributes:**
```ts
utils.createNodeFilter('[data-test-hide]')
```

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

**Combine multiple selectors:**
```ts
utils.createNodeFilter('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.
:::
19 changes: 19 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,25 @@ export const utils: {
* @experimental
*/
configurePrettyDOM(options: StringifyOptions): void
/**
* Creates a filter function for prettyDOM that filters out nodes based on CSS selectors.
* This is similar to Testing Library's defaultIgnore configuration.
*
* @example
* ```ts
* import { utils } from 'vitest/browser'
*
* // Filter out script, style, and elements with data-test-hide attribute
* utils.configurePrettyDOM({
* filterNode: utils.createNodeFilter('script, style, [data-test-hide]')
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.

Could we just make it accept a string for simplicity? (instead of a function). Then we can delete createNodeFilter

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.

Changed to accept string directly in commit 7c66451. filterNode now accepts string | ((node: any) => boolean). String values are CSS selectors, and the internal createNodeFilterFromSelector function handles the conversion. Removed createNodeFilter from exports.

* })
* ```
*
* @param selector - CSS selector to filter out (e.g., 'script, style, [data-test-hide]')
* @returns A filter function that can be used with configurePrettyDOM
* @experimental
*/
createNodeFilter(selector: string): (node: any) => boolean
/**
* Creates "Cannot find element" error. Useful for custom locators.
*/
Expand Down
43 changes: 43 additions & 0 deletions packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,10 +499,53 @@ function configurePrettyDOM(options: StringifyOptions) {
defaultOptions = options
}

/**
* Creates a filter function for prettyDOM that filters out nodes based on CSS selectors.
* This is similar to Testing Library's defaultIgnore configuration.
*
* @example
* ```ts
* import { utils } from 'vitest/browser'
*
* // Filter out script, style, and elements with data-test-hide attribute
* utils.configurePrettyDOM({
* filterNode: utils.createNodeFilter('script, style, [data-test-hide]')
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.

Can we just make it accept a string for simplicity?

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.

The function already accepts a string selector. The return type is a filter function because configurePrettyDOM expects filterNode to be a function (node: any) => boolean. This design matches Testing Library's approach and provides flexibility for users who want to implement custom filtering logic beyond CSS selectors.

* })
* ```
*
* @param selector - CSS selector to filter out (e.g., 'script, style, [data-test-hide]')
* @returns A filter function that can be used with configurePrettyDOM
* @experimental
*/
function createNodeFilter(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 utils = {
getElementError,
prettyDOM,
debug,
getElementLocatorSelectors,
configurePrettyDOM,
createNodeFilter,
}
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
Loading