diff --git a/README.md b/README.md index 1eb980a1..a8c85483 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ to maintain. - [Installation](#installation) - [Usage](#usage) - [Custom matchers](#custom-matchers) - - [`toBeInTheDOM`](#tobeinthedom) - [`toBeEmpty`](#tobeempty) + - [`toBeInTheDocument`](#tobeinthedocument) - [`toContainElement`](#tocontainelement) - [`toHaveTextContent`](#tohavetextcontent) - [`toHaveAttribute`](#tohaveattribute) @@ -55,6 +55,8 @@ to maintain. - [`toHaveFocus`](#tohavefocus) - [`toBeVisible`](#tobevisible) - [`toBeDisabled`](#tobedisabled) +- [Deprecated matchers](#deprecated-matchers) + - [`toBeInTheDOM`](#tobeinthedom) - [Inspiration](#inspiration) - [Other Solutions](#other-solutions) - [Guiding Principles](#guiding-principles) @@ -87,9 +89,9 @@ Alternatively, you can selectively import only the matchers you intend to use, and extend jest's `expect` yourself: ```javascript -import {toBeInTheDOM, toHaveClass} from 'jest-dom' +import {toBeInTheDocument, toHaveClass} from 'jest-dom' -expect.extend({toBeInTheDOM, toHaveClass}) +expect.extend({toBeInTheDocument, toHaveClass}) ``` > Note: when using TypeScript, this way of importing matchers won't provide the @@ -97,67 +99,52 @@ expect.extend({toBeInTheDOM, toHaveClass}) ## Custom matchers -### `toBeInTheDOM` +### `toBeEmpty` ```typescript -toBeInTheDOM(container?: HTMLElement | SVGElement) -``` - -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. - -#### Using the default DOM container - -```javascript -// add the custom expect matchers once -import 'jest-dom/extend-expect' - -// ... -// 2 -expect(queryByTestId(container, 'count-value')).toBeInTheDOM() -expect(queryByTestId(container, 'count-value1')).not.toBeInTheDOM() -// ... +toBeEmpty() ``` -#### Using a specified DOM container +This allows you to assert whether an element has content or not. ```javascript // add the custom expect matchers once import 'jest-dom/extend-expect' // ... -// -expect(queryByTestId(container, 'descendant')).toBeInTheDOM( - queryByTestId(container, 'ancestor'), -) -expect(queryByTestId(container, 'ancestor')).not.toBeInTheDOM( - queryByTestId(container, 'descendant'), -) +// +expect(queryByTestId(container, 'empty')).toBeEmpty() +expect(queryByTestId(container, 'not-empty')).not.toBeEmpty() // ... ``` -> Note: when using `toBeInTheDOM`, make sure you use a query function -> (like `queryByTestId`) rather than a get function (like `getByTestId`). -> Otherwise the `get*` function could throw an error before your assertion. - -### `toBeEmpty` +### `toBeInTheDocument` ```typescript -toBeEmpty() +toBeInTheDocument() ``` -This allows you to assert whether an element has content or not. +This allows you to assert whether an element is present in the document or not. ```javascript // add the custom expect matchers once import 'jest-dom/extend-expect' // ... -// -expect(queryByTestId(container, 'empty')).toBeEmpty() -expect(queryByTestId(container, 'not-empty')).not.toBeEmpty() +// document.body.innerHTML = `Html Element` + +// const htmlElement = document.querySelector('[data-testid="html-element"]') +// const svgElement = document.querySelector('[data-testid="html-element"]') +// const detachedElement = document.createElement('div') + +expect(htmlElement).toBeInTheDocument() +expect(svgElement).toBeInTheDocument() +expect(detacthedElement).not.toBeInTheDocument() // ... ``` +> 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) + ### `toContainElement` ```typescript @@ -389,6 +376,16 @@ expect(getByText(container, 'LINK')).not.toBeDisabled() // ... ``` +## Deprecated matchers + +### `toBeInTheDOM` + +> 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. + +> Please use [`toContainElement`](#tocontainelement) for searching a specific container. + +> Please use [`toBeInTheDocument`](#tobeinthedocument) for searching the entire document. + ## Inspiration This whole library was extracted out of Kent C. Dodds' [dom-testing-library][], diff --git a/extend-expect.d.ts b/extend-expect.d.ts index 5a059573..6f361223 100644 --- a/extend-expect.d.ts +++ b/extend-expect.d.ts @@ -1,6 +1,10 @@ declare namespace jest { interface Matchers { + /** + * @deprecated + */ toBeInTheDOM(container?: HTMLElement | SVGElement): R + toBeInTheDocument(): R toBeVisible(): R toBeEmpty(): R toBeDisabled(): R diff --git a/src/__tests__/to-be-in-the-document.js b/src/__tests__/to-be-in-the-document.js new file mode 100644 index 00000000..c8ebd0e6 --- /dev/null +++ b/src/__tests__/to-be-in-the-document.js @@ -0,0 +1,24 @@ +test('.toBeInTheDocument', () => { + document.body.innerHTML = ` + Html Element + ` + + const htmlElement = document.querySelector('[data-testid="html-element"]') + const svgElement = document.querySelector('[data-testid="html-element"]') + const detachedElement = document.createElement('div') + const fakeElement = {thisIsNot: 'an html element'} + const undefinedElement = undefined + const nullElement = null + + expect(htmlElement).toBeInTheDocument() + expect(svgElement).toBeInTheDocument() + expect(detachedElement).not.toBeInTheDocument() + + // negative test cases wrapped in throwError assertions for coverage. + expect(() => expect(htmlElement).not.toBeInTheDocument()).toThrowError() + expect(() => expect(svgElement).not.toBeInTheDocument()).toThrowError() + expect(() => expect(detachedElement).toBeInTheDocument()).toThrowError() + expect(() => expect(fakeElement).toBeInTheDocument()).toThrowError() + expect(() => expect(undefinedElement).toBeInTheDocument()).toThrowError() + expect(() => expect(nullElement).toBeInTheDocument()).toThrowError() +}) diff --git a/src/__tests__/to-be-in-the-dom.js b/src/__tests__/to-be-in-the-dom.js index 465baca6..4ef9fb48 100644 --- a/src/__tests__/to-be-in-the-dom.js +++ b/src/__tests__/to-be-in-the-dom.js @@ -1,6 +1,9 @@ import {render} from './helpers/test-utils' test('.toBeInTheDOM', () => { + // @deprecated intentionally hiding warnings for test clarity + const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}) + const {queryByTestId} = render(` @@ -51,4 +54,6 @@ test('.toBeInTheDOM', () => { expect(() => { expect(valueElement).toBeInTheDOM(fakeElement) }).toThrowError() + + spy.mockRestore() }) diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js new file mode 100644 index 00000000..1b24a704 --- /dev/null +++ b/src/__tests__/utils.js @@ -0,0 +1,43 @@ +import {checkDocumentKey, deprecate} from '../utils' + +function matcherMock() {} + +test('deprecate', () => { + const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}) + const name = 'test' + const replacement = 'test' + const message = `Warning: ${name} has been deprecated and will be removed in future updates.` + + deprecate(name, replacement) + expect(spy).toHaveBeenCalledWith(message, replacement) + + deprecate(name) + expect(spy).toHaveBeenCalledWith(message, undefined) + + spy.mockRestore() +}) + +test('checkDocumentKey', () => { + const fakeKey = 'fakeKey' + const realKey = 'documentElement' + const badKeyMessage = `${fakeKey} is undefined on document but is required to use ${ + matcherMock.name + }.` + + const badDocumentMessage = `document is undefined on global but is required to use ${ + matcherMock.name + }.` + + expect(() => + checkDocumentKey(document, realKey, matcherMock), + ).not.toThrowError() + + expect(() => { + checkDocumentKey(document, fakeKey, matcherMock) + }).toThrow(badKeyMessage) + + expect(() => { + //eslint-disable-next-line no-undef + checkDocumentKey(undefined, realKey, matcherMock) + }).toThrow(badDocumentMessage) +}) diff --git a/src/index.js b/src/index.js index 7f3b472e..a39b83ac 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ import {toBeInTheDOM} from './to-be-in-the-dom' +import {toBeInTheDocument} from './to-be-in-the-document' import {toBeEmpty} from './to-be-empty' import {toContainElement} from './to-contain-element' import {toHaveTextContent} from './to-have-text-content' @@ -11,6 +12,7 @@ import {toBeDisabled} from './to-be-disabled' export { toBeInTheDOM, + toBeInTheDocument, toBeEmpty, toContainElement, toHaveTextContent, diff --git a/src/to-be-in-the-document.js b/src/to-be-in-the-document.js new file mode 100644 index 00000000..9eb0487d --- /dev/null +++ b/src/to-be-in-the-document.js @@ -0,0 +1,25 @@ +import {matcherHint, printReceived} from 'jest-matcher-utils' +import {checkHtmlElement, checkDocumentKey} from './utils' + +export function toBeInTheDocument(element) { + checkHtmlElement(element, toBeInTheDocument, this) + checkDocumentKey(global.document, 'documentElement', toBeInTheDocument) + + return { + pass: document.documentElement.contains(element), + message: () => { + return [ + matcherHint( + `${this.isNot ? '.not' : ''}.toBeInTheDocument`, + 'element', + '', + ), + '', + 'Received:', + ` ${printReceived( + element.hasChildNodes() ? element.cloneNode(false) : element, + )}`, + ].join('\n') + }, + } +} diff --git a/src/to-be-in-the-dom.js b/src/to-be-in-the-dom.js index b413557b..c5c49edd 100644 --- a/src/to-be-in-the-dom.js +++ b/src/to-be-in-the-dom.js @@ -1,7 +1,12 @@ import {matcherHint, printReceived} from 'jest-matcher-utils' -import {checkHtmlElement} from './utils' +import {checkHtmlElement, deprecate} from './utils' export function toBeInTheDOM(element, container) { + deprecate( + 'toBeInTheDOM', + 'Please use toBeInTheDocument for searching the entire document and toContainElement for searching a specific container.', + ) + if (element) { checkHtmlElement(element, toBeInTheDOM, this) } diff --git a/src/utils.js b/src/utils.js index 7540bdcb..2bd89492 100644 --- a/src/utils.js +++ b/src/utils.js @@ -40,6 +40,39 @@ function checkHtmlElement(htmlElement, ...args) { } } +class InvalidDocumentError extends Error { + constructor(message, matcherFn) { + super() + + /* istanbul ignore next */ + if (Error.captureStackTrace) { + Error.captureStackTrace(this, matcherFn) + } + + this.message = message + } +} + +function checkDocumentKey(document, key, matcherFn) { + if (typeof document === 'undefined') { + throw new InvalidDocumentError( + `document is undefined on global but is required to use ${ + matcherFn.name + }.`, + matcherFn, + ) + } + + if (typeof document[key] === 'undefined') { + throw new InvalidDocumentError( + `${key} is undefined on document but is required to use ${ + matcherFn.name + }.`, + matcherFn, + ) + } +} + function display(value) { return typeof value === 'string' ? value : stringify(value) } @@ -66,4 +99,13 @@ function matches(textToMatch, node, matcher) { } } -export {checkHtmlElement, getMessage, matches} +function deprecate(name, replacementText) { + // Notify user that they are using deprecated functionality. + // eslint-disable-next-line no-console + console.warn( + `Warning: ${name} has been deprecated and will be removed in future updates.`, + replacementText, + ) +} + +export {checkDocumentKey, checkHtmlElement, deprecate, getMessage, matches}