Skip to content

Commit 9410e11

Browse files
feat: Add filterNode option to prettyDOM (#907)
Co-authored-by: Tim Deschryver <[email protected]>
1 parent 532106b commit 9410e11

13 files changed

+426
-16
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"chalk": "^4.1.0",
4747
"dom-accessibility-api": "^0.5.6",
4848
"lz-string": "^1.4.4",
49-
"pretty-format": "^26.6.2"
49+
"pretty-format": "^27.0.2"
5050
},
5151
"devDependencies": {
5252
"@testing-library/jest-dom": "^5.11.6",
@@ -63,6 +63,7 @@
6363
"plugin:import/typescript"
6464
],
6565
"rules": {
66+
"@typescript-eslint/prefer-includes": "off",
6667
"import/prefer-default-export": "off",
6768
"import/no-unassigned-import": "off",
6869
"import/no-useless-path-segments": "off",

src/DOMElementFilter.ts

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* Source: https://github.com/facebook/jest/blob/e7bb6a1e26ffab90611b2593912df15b69315611/packages/pretty-format/src/plugins/DOMElement.ts
3+
*/
4+
/* eslint-disable -- trying to stay as close to the original as possible */
5+
/* istanbul ignore file */
6+
import type {Config, NewPlugin, Printer, Refs} from 'pretty-format'
7+
8+
function escapeHTML(str: string): string {
9+
return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
10+
}
11+
// Return empty string if keys is empty.
12+
const printProps = (
13+
keys: Array<string>,
14+
props: Record<string, unknown>,
15+
config: Config,
16+
indentation: string,
17+
depth: number,
18+
refs: Refs,
19+
printer: Printer,
20+
): string => {
21+
const indentationNext = indentation + config.indent
22+
const colors = config.colors
23+
return keys
24+
.map(key => {
25+
const value = props[key]
26+
let printed = printer(value, config, indentationNext, depth, refs)
27+
28+
if (typeof value !== 'string') {
29+
if (printed.indexOf('\n') !== -1) {
30+
printed =
31+
config.spacingOuter +
32+
indentationNext +
33+
printed +
34+
config.spacingOuter +
35+
indentation
36+
}
37+
printed = '{' + printed + '}'
38+
}
39+
40+
return (
41+
config.spacingInner +
42+
indentation +
43+
colors.prop.open +
44+
key +
45+
colors.prop.close +
46+
'=' +
47+
colors.value.open +
48+
printed +
49+
colors.value.close
50+
)
51+
})
52+
.join('')
53+
}
54+
55+
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#node_type_constants
56+
const NodeTypeTextNode = 3
57+
58+
// Return empty string if children is empty.
59+
const printChildren = (
60+
children: Array<unknown>,
61+
config: Config,
62+
indentation: string,
63+
depth: number,
64+
refs: Refs,
65+
printer: Printer,
66+
): string =>
67+
children
68+
.map(child => {
69+
const printedChild =
70+
typeof child === 'string'
71+
? printText(child, config)
72+
: printer(child, config, indentation, depth, refs)
73+
74+
if (
75+
printedChild === '' &&
76+
typeof child === 'object' &&
77+
child !== null &&
78+
(child as Node).nodeType !== NodeTypeTextNode
79+
) {
80+
// A plugin serialized this Node to '' meaning we should ignore it.
81+
return ''
82+
}
83+
return config.spacingOuter + indentation + printedChild
84+
})
85+
.join('')
86+
87+
const printText = (text: string, config: Config): string => {
88+
const contentColor = config.colors.content
89+
return contentColor.open + escapeHTML(text) + contentColor.close
90+
}
91+
92+
const printComment = (comment: string, config: Config): string => {
93+
const commentColor = config.colors.comment
94+
return (
95+
commentColor.open +
96+
'<!--' +
97+
escapeHTML(comment) +
98+
'-->' +
99+
commentColor.close
100+
)
101+
}
102+
103+
// Separate the functions to format props, children, and element,
104+
// so a plugin could override a particular function, if needed.
105+
// Too bad, so sad: the traditional (but unnecessary) space
106+
// in a self-closing tagColor requires a second test of printedProps.
107+
const printElement = (
108+
type: string,
109+
printedProps: string,
110+
printedChildren: string,
111+
config: Config,
112+
indentation: string,
113+
): string => {
114+
const tagColor = config.colors.tag
115+
return (
116+
tagColor.open +
117+
'<' +
118+
type +
119+
(printedProps &&
120+
tagColor.close +
121+
printedProps +
122+
config.spacingOuter +
123+
indentation +
124+
tagColor.open) +
125+
(printedChildren
126+
? '>' +
127+
tagColor.close +
128+
printedChildren +
129+
config.spacingOuter +
130+
indentation +
131+
tagColor.open +
132+
'</' +
133+
type
134+
: (printedProps && !config.min ? '' : ' ') + '/') +
135+
'>' +
136+
tagColor.close
137+
)
138+
}
139+
140+
const printElementAsLeaf = (type: string, config: Config): string => {
141+
const tagColor = config.colors.tag
142+
return (
143+
tagColor.open +
144+
'<' +
145+
type +
146+
tagColor.close +
147+
' …' +
148+
tagColor.open +
149+
' />' +
150+
tagColor.close
151+
)
152+
}
153+
154+
const ELEMENT_NODE = 1
155+
const TEXT_NODE = 3
156+
const COMMENT_NODE = 8
157+
const FRAGMENT_NODE = 11
158+
159+
const ELEMENT_REGEXP = /^((HTML|SVG)\w*)?Element$/
160+
161+
const testNode = (val: any) => {
162+
const constructorName = val.constructor.name
163+
const {nodeType, tagName} = val
164+
const isCustomElement =
165+
(typeof tagName === 'string' && tagName.includes('-')) ||
166+
(typeof val.hasAttribute === 'function' && val.hasAttribute('is'))
167+
168+
return (
169+
(nodeType === ELEMENT_NODE &&
170+
(ELEMENT_REGEXP.test(constructorName) || isCustomElement)) ||
171+
(nodeType === TEXT_NODE && constructorName === 'Text') ||
172+
(nodeType === COMMENT_NODE && constructorName === 'Comment') ||
173+
(nodeType === FRAGMENT_NODE && constructorName === 'DocumentFragment')
174+
)
175+
}
176+
177+
export const test: NewPlugin['test'] = (val: any) =>
178+
val?.constructor?.name && testNode(val)
179+
180+
type HandledType = Element | Text | Comment | DocumentFragment
181+
182+
function nodeIsText(node: HandledType): node is Text {
183+
return node.nodeType === TEXT_NODE
184+
}
185+
186+
function nodeIsComment(node: HandledType): node is Comment {
187+
return node.nodeType === COMMENT_NODE
188+
}
189+
190+
function nodeIsFragment(node: HandledType): node is DocumentFragment {
191+
return node.nodeType === FRAGMENT_NODE
192+
}
193+
194+
export default function createDOMElementFilter(
195+
filterNode: (node: Node) => boolean,
196+
): NewPlugin {
197+
return {
198+
test: (val: any) => val?.constructor?.name && testNode(val),
199+
serialize: (
200+
node: HandledType,
201+
config: Config,
202+
indentation: string,
203+
depth: number,
204+
refs: Refs,
205+
printer: Printer,
206+
) => {
207+
if (nodeIsText(node)) {
208+
return printText(node.data, config)
209+
}
210+
211+
if (nodeIsComment(node)) {
212+
return printComment(node.data, config)
213+
}
214+
215+
const type = nodeIsFragment(node)
216+
? `DocumentFragment`
217+
: node.tagName.toLowerCase()
218+
219+
if (++depth > config.maxDepth) {
220+
return printElementAsLeaf(type, config)
221+
}
222+
223+
return printElement(
224+
type,
225+
printProps(
226+
nodeIsFragment(node)
227+
? []
228+
: Array.from(node.attributes)
229+
.map(attr => attr.name)
230+
.sort(),
231+
nodeIsFragment(node)
232+
? {}
233+
: Array.from(node.attributes).reduce<Record<string, string>>(
234+
(props, attribute) => {
235+
props[attribute.name] = attribute.value
236+
return props
237+
},
238+
{},
239+
),
240+
config,
241+
indentation + config.indent,
242+
depth,
243+
refs,
244+
printer,
245+
),
246+
printChildren(
247+
Array.prototype.slice
248+
.call(node.childNodes || node.children)
249+
.filter(filterNode),
250+
config,
251+
indentation + config.indent,
252+
depth,
253+
refs,
254+
printer,
255+
),
256+
config,
257+
indentation,
258+
)
259+
},
260+
}
261+
}

0 commit comments

Comments
 (0)