Skip to content

Commit 3601efd

Browse files
smacpherson64gnapse
authored andcommitted
feat: Introduce toBeInTheDocument, deprecate toBeInTheDOM (#40)
* #34: Added test for toBeInTheDocument * #34: Added toBeInTheDocument functionality * #34: Added deprecate test and functionality * #34: Updated toBeInTheDOM tests to hide console.warn for clarity * #34: Added deprecated notice to toBeInTheDOM * #34: Updated types (deprecated notice and toBeInTheDocument) * #34: Updated documentation * #34: Simplified deprecate util function * #34: Fixed grammar error * #34: Added document validation * #34: Cleaned up tests * #34: Updated test message for consistency * #34: Added null and undefined tests * #34: Updated README.md with better examples and clearer notes * #34: Improved element selector in documentation
1 parent f4991cd commit 3601efd

File tree

9 files changed

+187
-40
lines changed

9 files changed

+187
-40
lines changed

README.md

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ to maintain.
4545
- [Installation](#installation)
4646
- [Usage](#usage)
4747
- [Custom matchers](#custom-matchers)
48-
- [`toBeInTheDOM`](#tobeinthedom)
4948
- [`toBeEmpty`](#tobeempty)
49+
- [`toBeInTheDocument`](#tobeinthedocument)
5050
- [`toContainElement`](#tocontainelement)
5151
- [`toHaveTextContent`](#tohavetextcontent)
5252
- [`toHaveAttribute`](#tohaveattribute)
@@ -55,6 +55,8 @@ to maintain.
5555
- [`toHaveFocus`](#tohavefocus)
5656
- [`toBeVisible`](#tobevisible)
5757
- [`toBeDisabled`](#tobedisabled)
58+
- [Deprecated matchers](#deprecated-matchers)
59+
- [`toBeInTheDOM`](#tobeinthedom)
5860
- [Inspiration](#inspiration)
5961
- [Other Solutions](#other-solutions)
6062
- [Guiding Principles](#guiding-principles)
@@ -87,77 +89,62 @@ Alternatively, you can selectively import only the matchers you intend to use,
8789
and extend jest's `expect` yourself:
8890

8991
```javascript
90-
import {toBeInTheDOM, toHaveClass} from 'jest-dom'
92+
import {toBeInTheDocument, toHaveClass} from 'jest-dom'
9193

92-
expect.extend({toBeInTheDOM, toHaveClass})
94+
expect.extend({toBeInTheDocument, toHaveClass})
9395
```
9496

9597
> Note: when using TypeScript, this way of importing matchers won't provide the
9698
> necessary type definitions. More on this [here](https://github.com/gnapse/jest-dom/pull/11#issuecomment-387817459).
9799
98100
## Custom matchers
99101

100-
### `toBeInTheDOM`
102+
### `toBeEmpty`
101103

102104
```typescript
103-
toBeInTheDOM(container?: HTMLElement | SVGElement)
104-
```
105-
106-
This allows you to assert whether an element present in the DOM container or not. If no DOM container is specified it will use the default DOM context.
107-
108-
#### Using the default DOM container
109-
110-
```javascript
111-
// add the custom expect matchers once
112-
import 'jest-dom/extend-expect'
113-
114-
// ...
115-
// <span data-testid="count-value">2</span>
116-
expect(queryByTestId(container, 'count-value')).toBeInTheDOM()
117-
expect(queryByTestId(container, 'count-value1')).not.toBeInTheDOM()
118-
// ...
105+
toBeEmpty()
119106
```
120107

121-
#### Using a specified DOM container
108+
This allows you to assert whether an element has content or not.
122109

123110
```javascript
124111
// add the custom expect matchers once
125112
import 'jest-dom/extend-expect'
126113

127114
// ...
128-
// <span data-testid="ancestor"><span data-testid="descendant"></span></span>
129-
expect(queryByTestId(container, 'descendant')).toBeInTheDOM(
130-
queryByTestId(container, 'ancestor'),
131-
)
132-
expect(queryByTestId(container, 'ancestor')).not.toBeInTheDOM(
133-
queryByTestId(container, 'descendant'),
134-
)
115+
// <span data-testid="not-empty"><span data-testid="empty"></span></span>
116+
expect(queryByTestId(container, 'empty')).toBeEmpty()
117+
expect(queryByTestId(container, 'not-empty')).not.toBeEmpty()
135118
// ...
136119
```
137120

138-
> Note: when using `toBeInTheDOM`, make sure you use a query function
139-
> (like `queryByTestId`) rather than a get function (like `getByTestId`).
140-
> Otherwise the `get*` function could throw an error before your assertion.
141-
142-
### `toBeEmpty`
121+
### `toBeInTheDocument`
143122

144123
```typescript
145-
toBeEmpty()
124+
toBeInTheDocument()
146125
```
147126

148-
This allows you to assert whether an element has content or not.
127+
This allows you to assert whether an element is present in the document or not.
149128

150129
```javascript
151130
// add the custom expect matchers once
152131
import 'jest-dom/extend-expect'
153132

154133
// ...
155-
// <span data-testid="not-empty"><span data-testid="empty"></span></span>
156-
expect(queryByTestId(container, 'empty')).toBeEmpty()
157-
expect(queryByTestId(container, 'not-empty')).not.toBeEmpty()
134+
// document.body.innerHTML = `<span data-testid="html-element"><span>Html Element</span></span><svg data-testid="svg-element"></svg>`
135+
136+
// const htmlElement = document.querySelector('[data-testid="html-element"]')
137+
// const svgElement = document.querySelector('[data-testid="html-element"]')
138+
// const detachedElement = document.createElement('div')
139+
140+
expect(htmlElement).toBeInTheDocument()
141+
expect(svgElement).toBeInTheDocument()
142+
expect(detacthedElement).not.toBeInTheDocument()
158143
// ...
159144
```
160145

146+
> Note: This will not find detached elements. The element must be added to the document to be found. If you desire to search in a detached element please use: [`toContainElement`](#tocontainelement)
147+
161148
### `toContainElement`
162149

163150
```typescript
@@ -389,6 +376,16 @@ expect(getByText(container, 'LINK')).not.toBeDisabled()
389376
// ...
390377
```
391378

379+
## Deprecated matchers
380+
381+
### `toBeInTheDOM`
382+
383+
> Note: The differences between `toBeInTheDOM` and `toBeInTheDocument` are significant. Replacing all uses of `toBeInTheDOM` with `toBeInTheDocument` will likely cause unintended consequences in your tests. Please make sure when replacing `toBeInTheDOM` to read through the replacements below to see which use case works better for your needs.
384+
385+
> Please use [`toContainElement`](#tocontainelement) for searching a specific container.
386+
387+
> Please use [`toBeInTheDocument`](#tobeinthedocument) for searching the entire document.
388+
392389
## Inspiration
393390

394391
This whole library was extracted out of Kent C. Dodds' [dom-testing-library][],

extend-expect.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
declare namespace jest {
22
interface Matchers<R> {
3+
/**
4+
* @deprecated
5+
*/
36
toBeInTheDOM(container?: HTMLElement | SVGElement): R
7+
toBeInTheDocument(): R
48
toBeVisible(): R
59
toBeEmpty(): R
610
toBeDisabled(): R
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
test('.toBeInTheDocument', () => {
2+
document.body.innerHTML = `
3+
<span data-testid="html-element"><span>Html Element</span></span>
4+
<svg data-testid="svg-element"></svg>`
5+
6+
const htmlElement = document.querySelector('[data-testid="html-element"]')
7+
const svgElement = document.querySelector('[data-testid="html-element"]')
8+
const detachedElement = document.createElement('div')
9+
const fakeElement = {thisIsNot: 'an html element'}
10+
const undefinedElement = undefined
11+
const nullElement = null
12+
13+
expect(htmlElement).toBeInTheDocument()
14+
expect(svgElement).toBeInTheDocument()
15+
expect(detachedElement).not.toBeInTheDocument()
16+
17+
// negative test cases wrapped in throwError assertions for coverage.
18+
expect(() => expect(htmlElement).not.toBeInTheDocument()).toThrowError()
19+
expect(() => expect(svgElement).not.toBeInTheDocument()).toThrowError()
20+
expect(() => expect(detachedElement).toBeInTheDocument()).toThrowError()
21+
expect(() => expect(fakeElement).toBeInTheDocument()).toThrowError()
22+
expect(() => expect(undefinedElement).toBeInTheDocument()).toThrowError()
23+
expect(() => expect(nullElement).toBeInTheDocument()).toThrowError()
24+
})

src/__tests__/to-be-in-the-dom.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {render} from './helpers/test-utils'
22

33
test('.toBeInTheDOM', () => {
4+
// @deprecated intentionally hiding warnings for test clarity
5+
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
6+
47
const {queryByTestId} = render(`
58
<span data-testid="count-container">
69
<span data-testid="count-value"></span>
@@ -51,4 +54,6 @@ test('.toBeInTheDOM', () => {
5154
expect(() => {
5255
expect(valueElement).toBeInTheDOM(fakeElement)
5356
}).toThrowError()
57+
58+
spy.mockRestore()
5459
})

src/__tests__/utils.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {checkDocumentKey, deprecate} from '../utils'
2+
3+
function matcherMock() {}
4+
5+
test('deprecate', () => {
6+
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
7+
const name = 'test'
8+
const replacement = 'test'
9+
const message = `Warning: ${name} has been deprecated and will be removed in future updates.`
10+
11+
deprecate(name, replacement)
12+
expect(spy).toHaveBeenCalledWith(message, replacement)
13+
14+
deprecate(name)
15+
expect(spy).toHaveBeenCalledWith(message, undefined)
16+
17+
spy.mockRestore()
18+
})
19+
20+
test('checkDocumentKey', () => {
21+
const fakeKey = 'fakeKey'
22+
const realKey = 'documentElement'
23+
const badKeyMessage = `${fakeKey} is undefined on document but is required to use ${
24+
matcherMock.name
25+
}.`
26+
27+
const badDocumentMessage = `document is undefined on global but is required to use ${
28+
matcherMock.name
29+
}.`
30+
31+
expect(() =>
32+
checkDocumentKey(document, realKey, matcherMock),
33+
).not.toThrowError()
34+
35+
expect(() => {
36+
checkDocumentKey(document, fakeKey, matcherMock)
37+
}).toThrow(badKeyMessage)
38+
39+
expect(() => {
40+
//eslint-disable-next-line no-undef
41+
checkDocumentKey(undefined, realKey, matcherMock)
42+
}).toThrow(badDocumentMessage)
43+
})

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {toBeInTheDOM} from './to-be-in-the-dom'
2+
import {toBeInTheDocument} from './to-be-in-the-document'
23
import {toBeEmpty} from './to-be-empty'
34
import {toContainElement} from './to-contain-element'
45
import {toHaveTextContent} from './to-have-text-content'
@@ -11,6 +12,7 @@ import {toBeDisabled} from './to-be-disabled'
1112

1213
export {
1314
toBeInTheDOM,
15+
toBeInTheDocument,
1416
toBeEmpty,
1517
toContainElement,
1618
toHaveTextContent,

src/to-be-in-the-document.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {matcherHint, printReceived} from 'jest-matcher-utils'
2+
import {checkHtmlElement, checkDocumentKey} from './utils'
3+
4+
export function toBeInTheDocument(element) {
5+
checkHtmlElement(element, toBeInTheDocument, this)
6+
checkDocumentKey(global.document, 'documentElement', toBeInTheDocument)
7+
8+
return {
9+
pass: document.documentElement.contains(element),
10+
message: () => {
11+
return [
12+
matcherHint(
13+
`${this.isNot ? '.not' : ''}.toBeInTheDocument`,
14+
'element',
15+
'',
16+
),
17+
'',
18+
'Received:',
19+
` ${printReceived(
20+
element.hasChildNodes() ? element.cloneNode(false) : element,
21+
)}`,
22+
].join('\n')
23+
},
24+
}
25+
}

src/to-be-in-the-dom.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import {matcherHint, printReceived} from 'jest-matcher-utils'
2-
import {checkHtmlElement} from './utils'
2+
import {checkHtmlElement, deprecate} from './utils'
33

44
export function toBeInTheDOM(element, container) {
5+
deprecate(
6+
'toBeInTheDOM',
7+
'Please use toBeInTheDocument for searching the entire document and toContainElement for searching a specific container.',
8+
)
9+
510
if (element) {
611
checkHtmlElement(element, toBeInTheDOM, this)
712
}

src/utils.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,39 @@ function checkHtmlElement(htmlElement, ...args) {
4040
}
4141
}
4242

43+
class InvalidDocumentError extends Error {
44+
constructor(message, matcherFn) {
45+
super()
46+
47+
/* istanbul ignore next */
48+
if (Error.captureStackTrace) {
49+
Error.captureStackTrace(this, matcherFn)
50+
}
51+
52+
this.message = message
53+
}
54+
}
55+
56+
function checkDocumentKey(document, key, matcherFn) {
57+
if (typeof document === 'undefined') {
58+
throw new InvalidDocumentError(
59+
`document is undefined on global but is required to use ${
60+
matcherFn.name
61+
}.`,
62+
matcherFn,
63+
)
64+
}
65+
66+
if (typeof document[key] === 'undefined') {
67+
throw new InvalidDocumentError(
68+
`${key} is undefined on document but is required to use ${
69+
matcherFn.name
70+
}.`,
71+
matcherFn,
72+
)
73+
}
74+
}
75+
4376
function display(value) {
4477
return typeof value === 'string' ? value : stringify(value)
4578
}
@@ -66,4 +99,13 @@ function matches(textToMatch, node, matcher) {
6699
}
67100
}
68101

69-
export {checkHtmlElement, getMessage, matches}
102+
function deprecate(name, replacementText) {
103+
// Notify user that they are using deprecated functionality.
104+
// eslint-disable-next-line no-console
105+
console.warn(
106+
`Warning: ${name} has been deprecated and will be removed in future updates.`,
107+
replacementText,
108+
)
109+
}
110+
111+
export {checkDocumentKey, checkHtmlElement, deprecate, getMessage, matches}

0 commit comments

Comments
 (0)