Skip to content

Commit fa0d91d

Browse files
julienwgnapse
andauthoredApr 22, 2021
feat: check for any Node in toHaveTextContent (#358)
Co-authored-by: Ernesto García <gnapse@gmail.com>
1 parent 7d1c742 commit fa0d91d

File tree

6 files changed

+145
-14
lines changed

6 files changed

+145
-14
lines changed
 

‎README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -748,10 +748,11 @@ The usual rules of css precedence apply.
748748
toHaveTextContent(text: string | RegExp, options?: {normalizeWhitespace: boolean})
749749
```
750750

751-
This allows you to check whether the given element has a text content or not.
751+
This allows you to check whether the given node has a text content or not. This
752+
supports elements, but also text nodes and fragments.
752753

753754
When a `string` argument is passed through, it will perform a partial
754-
case-sensitive match to the element content.
755+
case-sensitive match to the node content.
755756

756757
To perform a case-insensitive match, you can use a `RegExp` with the `/i`
757758
modifier.

‎src/__tests__/helpers/test-utils.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ function render(html) {
55
container.innerHTML = html
66
const queryByTestId = testId =>
77
container.querySelector(`[data-testid="${testId}"]`)
8+
// asFragment has been stolen from react-testing-library
9+
const asFragment = () =>
10+
document.createRange().createContextualFragment(container.innerHTML)
811

912
// Some tests need to look up global ids with document.getElementById()
1013
// so we need to be inside an actual document.
1114
document.body.innerHTML = ''
1215
document.body.appendChild(container)
1316

14-
return {container, queryByTestId}
17+
return {container, queryByTestId, asFragment}
1518
}
1619

1720
export {render}

‎src/__tests__/to-have-text-content.js

+14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ describe('.toHaveTextContent', () => {
1010
expect(queryByTestId('count-value')).not.toHaveTextContent('21')
1111
})
1212

13+
test('handles text nodes', () => {
14+
const {container} = render(`<span>example</span>`)
15+
16+
expect(container.querySelector('span').firstChild).toHaveTextContent(
17+
'example',
18+
)
19+
})
20+
21+
test('handles fragments', () => {
22+
const {asFragment} = render(`<span>example</span>`)
23+
24+
expect(asFragment()).toHaveTextContent('example')
25+
})
26+
1327
test('handles negative test cases', () => {
1428
const {queryByTestId} = render(`<span data-testid="count-value">2</span>`)
1529

‎src/__tests__/utils.js

+90
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
deprecate,
33
checkHtmlElement,
4+
checkNode,
45
HtmlElementTypeError,
6+
NodeTypeError,
57
toSentence,
68
} from '../utils'
79
import document from './helpers/document'
@@ -95,6 +97,94 @@ describe('checkHtmlElement', () => {
9597
})
9698
})
9799

100+
describe('checkNode', () => {
101+
let assertionContext
102+
beforeAll(() => {
103+
expect.extend({
104+
fakeMatcher() {
105+
assertionContext = this
106+
107+
return {pass: true}
108+
},
109+
})
110+
111+
expect(true).fakeMatcher(true)
112+
})
113+
it('does not throw an error for correct html element', () => {
114+
expect(() => {
115+
const element = document.createElement('p')
116+
checkNode(element, () => {}, assertionContext)
117+
}).not.toThrow()
118+
})
119+
120+
it('does not throw an error for correct svg element', () => {
121+
expect(() => {
122+
const element = document.createElementNS(
123+
'http://www.w3.org/2000/svg',
124+
'rect',
125+
)
126+
checkNode(element, () => {}, assertionContext)
127+
}).not.toThrow()
128+
})
129+
130+
it('does not throw an error for Document fragments', () => {
131+
expect(() => {
132+
const fragment = document.createDocumentFragment()
133+
checkNode(fragment, () => {}, assertionContext)
134+
}).not.toThrow()
135+
})
136+
137+
it('does not throw an error for text nodes', () => {
138+
expect(() => {
139+
const text = document.createTextNode('foo')
140+
checkNode(text, () => {}, assertionContext)
141+
}).not.toThrow()
142+
})
143+
144+
it('does not throw for body', () => {
145+
expect(() => {
146+
checkNode(document.body, () => {}, assertionContext)
147+
}).not.toThrow()
148+
})
149+
150+
it('throws for undefined', () => {
151+
expect(() => {
152+
checkNode(undefined, () => {}, assertionContext)
153+
}).toThrow(NodeTypeError)
154+
})
155+
156+
it('throws for document', () => {
157+
expect(() => {
158+
checkNode(document, () => {}, assertionContext)
159+
}).toThrow(NodeTypeError)
160+
})
161+
162+
it('throws for function', () => {
163+
expect(() => {
164+
checkNode(
165+
() => {},
166+
() => {},
167+
assertionContext,
168+
)
169+
}).toThrow(NodeTypeError)
170+
})
171+
172+
it('throws for almost element-like objects', () => {
173+
class FakeObject {}
174+
expect(() => {
175+
checkNode(
176+
{
177+
ownerDocument: {
178+
defaultView: {Node: FakeObject, SVGElement: FakeObject},
179+
},
180+
},
181+
() => {},
182+
assertionContext,
183+
)
184+
}).toThrow(NodeTypeError)
185+
})
186+
})
187+
98188
describe('toSentence', () => {
99189
it('turns array into string of comma separated list with default last word connector', () => {
100190
expect(toSentence(['one', 'two', 'three'])).toBe('one, two and three')

‎src/to-have-text-content.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import {checkHtmlElement, getMessage, matches, normalize} from './utils'
1+
import {getMessage, checkNode, matches, normalize} from './utils'
22

33
export function toHaveTextContent(
4-
htmlElement,
4+
node,
55
checkWith,
66
options = {normalizeWhitespace: true},
77
) {
8-
checkHtmlElement(htmlElement, toHaveTextContent, this)
8+
checkNode(node, toHaveTextContent, this)
99

1010
const textContent = options.normalizeWhitespace
11-
? normalize(htmlElement.textContent)
12-
: htmlElement.textContent.replace(/\u00a0/g, ' ') // Replace &nbsp; with normal spaces
11+
? normalize(node.textContent)
12+
: node.textContent.replace(/\u00a0/g, ' ') // Replace &nbsp; with normal spaces
1313

1414
const checkingWithEmptyString = textContent !== '' && checkWith === ''
1515

‎src/utils.js

+29-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import redent from 'redent'
22
import {parse} from 'css'
33
import isEqual from 'lodash/isEqual'
44

5-
class HtmlElementTypeError extends Error {
6-
constructor(received, matcherFn, context) {
5+
class GenericTypeError extends Error {
6+
constructor(expectedString, received, matcherFn, context) {
77
super()
88

99
/* istanbul ignore next */
@@ -31,24 +31,45 @@ class HtmlElementTypeError extends Error {
3131
// eslint-disable-next-line babel/new-cap
3232
`${context.utils.RECEIVED_COLOR(
3333
'received',
34-
)} value must be an HTMLElement or an SVGElement.`,
34+
)} value must ${expectedString}.`,
3535
withType,
3636
].join('\n')
3737
}
3838
}
3939

40-
function checkHasWindow(htmlElement, ...args) {
40+
class HtmlElementTypeError extends GenericTypeError {
41+
constructor(...args) {
42+
super('be an HTMLElement or an SVGElement', ...args)
43+
}
44+
}
45+
46+
class NodeTypeError extends GenericTypeError {
47+
constructor(...args) {
48+
super('be a Node', ...args)
49+
}
50+
}
51+
52+
function checkHasWindow(htmlElement, ErrorClass, ...args) {
4153
if (
4254
!htmlElement ||
4355
!htmlElement.ownerDocument ||
4456
!htmlElement.ownerDocument.defaultView
4557
) {
46-
throw new HtmlElementTypeError(htmlElement, ...args)
58+
throw new ErrorClass(htmlElement, ...args)
59+
}
60+
}
61+
62+
function checkNode(node, ...args) {
63+
checkHasWindow(node, NodeTypeError, ...args)
64+
const window = node.ownerDocument.defaultView
65+
66+
if (!(node instanceof window.Node)) {
67+
throw new NodeTypeError(node, ...args)
4768
}
4869
}
4970

5071
function checkHtmlElement(htmlElement, ...args) {
51-
checkHasWindow(htmlElement, ...args)
72+
checkHasWindow(htmlElement, HtmlElementTypeError, ...args)
5273
const window = htmlElement.ownerDocument.defaultView
5374

5475
if (
@@ -209,7 +230,9 @@ function toSentence(
209230

210231
export {
211232
HtmlElementTypeError,
233+
NodeTypeError,
212234
checkHtmlElement,
235+
checkNode,
213236
parseCSS,
214237
deprecate,
215238
getMessage,

0 commit comments

Comments
 (0)
Please sign in to comment.