Skip to content

Commit

Permalink
feat: check for any Node in toHaveTextContent (#358)
Browse files Browse the repository at this point in the history
Co-authored-by: Ernesto García <gnapse@gmail.com>
  • Loading branch information
julienw and gnapse committed Apr 22, 2021
1 parent 7d1c742 commit fa0d91d
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 14 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,10 +748,11 @@ The usual rules of css precedence apply.
toHaveTextContent(text: string | RegExp, options?: {normalizeWhitespace: boolean})
```

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

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

To perform a case-insensitive match, you can use a `RegExp` with the `/i`
modifier.
Expand Down
5 changes: 4 additions & 1 deletion src/__tests__/helpers/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ function render(html) {
container.innerHTML = html
const queryByTestId = testId =>
container.querySelector(`[data-testid="${testId}"]`)
// asFragment has been stolen from react-testing-library
const asFragment = () =>
document.createRange().createContextualFragment(container.innerHTML)

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

return {container, queryByTestId}
return {container, queryByTestId, asFragment}
}

export {render}
14 changes: 14 additions & 0 deletions src/__tests__/to-have-text-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ describe('.toHaveTextContent', () => {
expect(queryByTestId('count-value')).not.toHaveTextContent('21')
})

test('handles text nodes', () => {
const {container} = render(`<span>example</span>`)

expect(container.querySelector('span').firstChild).toHaveTextContent(
'example',
)
})

test('handles fragments', () => {
const {asFragment} = render(`<span>example</span>`)

expect(asFragment()).toHaveTextContent('example')
})

test('handles negative test cases', () => {
const {queryByTestId} = render(`<span data-testid="count-value">2</span>`)

Expand Down
90 changes: 90 additions & 0 deletions src/__tests__/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
deprecate,
checkHtmlElement,
checkNode,
HtmlElementTypeError,
NodeTypeError,
toSentence,
} from '../utils'
import document from './helpers/document'
Expand Down Expand Up @@ -95,6 +97,94 @@ describe('checkHtmlElement', () => {
})
})

describe('checkNode', () => {
let assertionContext
beforeAll(() => {
expect.extend({
fakeMatcher() {
assertionContext = this

return {pass: true}
},
})

expect(true).fakeMatcher(true)
})
it('does not throw an error for correct html element', () => {
expect(() => {
const element = document.createElement('p')
checkNode(element, () => {}, assertionContext)
}).not.toThrow()
})

it('does not throw an error for correct svg element', () => {
expect(() => {
const element = document.createElementNS(
'http://www.w3.org/2000/svg',
'rect',
)
checkNode(element, () => {}, assertionContext)
}).not.toThrow()
})

it('does not throw an error for Document fragments', () => {
expect(() => {
const fragment = document.createDocumentFragment()
checkNode(fragment, () => {}, assertionContext)
}).not.toThrow()
})

it('does not throw an error for text nodes', () => {
expect(() => {
const text = document.createTextNode('foo')
checkNode(text, () => {}, assertionContext)
}).not.toThrow()
})

it('does not throw for body', () => {
expect(() => {
checkNode(document.body, () => {}, assertionContext)
}).not.toThrow()
})

it('throws for undefined', () => {
expect(() => {
checkNode(undefined, () => {}, assertionContext)
}).toThrow(NodeTypeError)
})

it('throws for document', () => {
expect(() => {
checkNode(document, () => {}, assertionContext)
}).toThrow(NodeTypeError)
})

it('throws for function', () => {
expect(() => {
checkNode(
() => {},
() => {},
assertionContext,
)
}).toThrow(NodeTypeError)
})

it('throws for almost element-like objects', () => {
class FakeObject {}
expect(() => {
checkNode(
{
ownerDocument: {
defaultView: {Node: FakeObject, SVGElement: FakeObject},
},
},
() => {},
assertionContext,
)
}).toThrow(NodeTypeError)
})
})

describe('toSentence', () => {
it('turns array into string of comma separated list with default last word connector', () => {
expect(toSentence(['one', 'two', 'three'])).toBe('one, two and three')
Expand Down
10 changes: 5 additions & 5 deletions src/to-have-text-content.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {checkHtmlElement, getMessage, matches, normalize} from './utils'
import {getMessage, checkNode, matches, normalize} from './utils'

export function toHaveTextContent(
htmlElement,
node,
checkWith,
options = {normalizeWhitespace: true},
) {
checkHtmlElement(htmlElement, toHaveTextContent, this)
checkNode(node, toHaveTextContent, this)

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

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

Expand Down
35 changes: 29 additions & 6 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import redent from 'redent'
import {parse} from 'css'
import isEqual from 'lodash/isEqual'

class HtmlElementTypeError extends Error {
constructor(received, matcherFn, context) {
class GenericTypeError extends Error {
constructor(expectedString, received, matcherFn, context) {
super()

/* istanbul ignore next */
Expand Down Expand Up @@ -31,24 +31,45 @@ class HtmlElementTypeError extends Error {
// eslint-disable-next-line babel/new-cap
`${context.utils.RECEIVED_COLOR(
'received',
)} value must be an HTMLElement or an SVGElement.`,
)} value must ${expectedString}.`,
withType,
].join('\n')
}
}

function checkHasWindow(htmlElement, ...args) {
class HtmlElementTypeError extends GenericTypeError {
constructor(...args) {
super('be an HTMLElement or an SVGElement', ...args)
}
}

class NodeTypeError extends GenericTypeError {
constructor(...args) {
super('be a Node', ...args)
}
}

function checkHasWindow(htmlElement, ErrorClass, ...args) {
if (
!htmlElement ||
!htmlElement.ownerDocument ||
!htmlElement.ownerDocument.defaultView
) {
throw new HtmlElementTypeError(htmlElement, ...args)
throw new ErrorClass(htmlElement, ...args)
}
}

function checkNode(node, ...args) {
checkHasWindow(node, NodeTypeError, ...args)
const window = node.ownerDocument.defaultView

if (!(node instanceof window.Node)) {
throw new NodeTypeError(node, ...args)
}
}

function checkHtmlElement(htmlElement, ...args) {
checkHasWindow(htmlElement, ...args)
checkHasWindow(htmlElement, HtmlElementTypeError, ...args)
const window = htmlElement.ownerDocument.defaultView

if (
Expand Down Expand Up @@ -209,7 +230,9 @@ function toSentence(

export {
HtmlElementTypeError,
NodeTypeError,
checkHtmlElement,
checkNode,
parseCSS,
deprecate,
getMessage,
Expand Down

0 comments on commit fa0d91d

Please sign in to comment.