From f2683d94c9e92168c7d00218f0e7af14d597ee18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 10 Apr 2018 19:02:52 -0300 Subject: [PATCH] feat: custom jest matchers from jest-dom (#15) * Use custom jest matchers from jest-dom * Fix imports in tests --- README.md | 118 ++++----------------- extend-expect.js | 2 +- package.json | 2 +- src/__tests__/element-queries.js | 2 +- src/__tests__/example.js | 2 +- src/__tests__/wait-for-element.js | 2 +- src/extend-expect.js | 9 -- src/jest-extensions.js | 169 ------------------------------ 8 files changed, 28 insertions(+), 278 deletions(-) delete mode 100644 src/extend-expect.js delete mode 100644 src/jest-extensions.js diff --git a/README.md b/README.md index c6b1d1ae..9874d072 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ your team down. The `dom-testing-library` is a very light-weight solution for testing DOM nodes (whether simulated with [`JSDOM`](https://github.com/jsdom/jsdom) as provided by -default with [jest](https://facebook.github.io/jest) or in the browser). The -main utilities it provides involve querying the DOM for nodes in a way that's -similar to how the user finds elements on the page. In this way, the library -helps ensure your tests give you confidence in your UI code. The -`dom-testing-library`'s primary guiding principle is: +default with [jest][] or in the browser). The main utilities it provides involve +querying the DOM for nodes in a way that's similar to how the user finds +elements on the page. In this way, the library helps ensure your tests give you +confidence in your UI code. The `dom-testing-library`'s primary guiding +principle is: > [The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle] @@ -80,11 +80,7 @@ when a real user uses it. * [`waitForElement`](#waitforelement) * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event) * [Custom Jest Matchers](#custom-jest-matchers) - * [`toBeInTheDOM`](#tobeinthedom) - * [`toHaveTextContent`](#tohavetextcontent) - * [`toHaveAttribute`](#tohaveattribute) - * [`toHaveClass`](#tohaveclass) - * [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript) + * [Using other assertion libraries](#using-other-assertion-libraries) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Debugging](#debugging) @@ -400,106 +396,37 @@ fireEvent.click(getElementByText('Submit'), rightClick) ## Custom Jest Matchers -There are two simple API which extend the `expect` API of jest for making assertions easier. - -### `toBeInTheDOM` - -This allows you to assert whether an element present in the DOM or not. +When using [jest][], we recommend that you import a set of custom matchers that +make it easier to check several aspects of the state of a DOM element. These are +provided by [jest-dom](https://github.com/gnapse/jest-dom), but are also +included for convenience to be imported from this library directly: ```javascript -// add the custom expect matchers import 'dom-testing-library/extend-expect' +// Hello World +expect(queryByText(container, 'greetings')).toBeInTheDOM() +expect(queryByText(container, 'greetings')).not.toHaveTextContent('Bye bye') // ... -// 2 -expect(queryByTestId(container, 'count-value')).toBeInTheDOM() -expect(queryByTestId(container, 'count-value1')).not.toBeInTheDOM() -// ... -``` > 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. - -### `toHaveTextContent` - -This API allows you to check whether the given element has a text content or not. - -```javascript -// add the custom expect matchers -import 'dom-testing-library/extend-expect' - -// ... -// 2 -expect(getByTestId(container, 'count-value')).toHaveTextContent('2') -expect(getByTestId(container, 'count-value')).not.toHaveTextContent('21') -// ... -``` - -### `toHaveAttribute` - -This allows you to check wether the given element has an attribute or not. You -can also optionally check that the attribute has a specific expected value. - -```javascript -// add the custom expect matchers -import 'dom-testing-library/extend-expect' - -// ... -// -expect(getByTestId(container, 'ok-button')).toHaveAttribute('disabled') -expect(getByTestId(container, 'ok-button')).toHaveAttribute('type', 'submit') -expect(getByTestId(container, 'ok-button')).not.toHaveAttribute( - 'type', - 'button', -) -// ... ``` -### `toHaveClass` +Check out [jest-dom's documentation](https://github.com/gnapse/jest-dom#readme) +for a full list of available matchers. -This allows you to check wether the given element has certain classes within its -`class` attribute. +### Using other assertion libraries -```javascript -// add the custom expect matchers -import 'dom-testing-library/extend-expect' +If you're not using jest, you may be able to find a similar set of custom +assertions for your library of choice. Here's a list of alternatives to jest-dom +for other popular assertion libraries: -// ... -// -expect(getByTestId(container, 'delete-button')).toHaveClass('extra') -expect(getByTestId(container, 'delete-button')).toHaveClass('btn-danger btn') -expect(getByTestId(container, 'delete-button')).not.toHaveClass('btn-link') -// ... -``` - -### Custom Jest Matchers - Typescript +* [chai-dom](https://github.com/nathanboktae/chai-dom) -When you use custom Jest Matchers with Typescript, you will need to extend the -type signature of `jest.Matchers`, then cast the result of `expect` -accordingly. Here's a handy usage example: - -```typescript -import {getByTestId} from 'dom-testing-library' -// this adds custom expect matchers -import 'dom-testing-library/extend-expect' -interface ExtendedMatchers extends jest.Matchers { - toHaveTextContent: (htmlElement: string) => object - toBeInTheDOM: () => void -} -test('renders the tooltip as expected', async () => { - // however you render it: - // render(`
hello world
`) - ;(expect( - container, - getByTestId('greeting'), - ) as ExtendedMatchers).toHaveTextContent('hello world') -}) -``` +If you're aware of some other alternatives, please [make a pull request][prs] +and add it here! ## `TextMatch` @@ -768,3 +695,4 @@ MIT [set-immediate]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate [guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106 [data-testid-blog-post]: https://blog.kentcdodds.com/making-your-ui-tests-resilient-to-change-d37a6ee37269 +[jest]: https://facebook.github.io/jest diff --git a/extend-expect.js b/extend-expect.js index e7d19c10..5d5590f5 100644 --- a/extend-expect.js +++ b/extend-expect.js @@ -1,2 +1,2 @@ // eslint-disable-next-line -require('./dist/extend-expect') +require('jest-dom/extend-expect') diff --git a/package.json b/package.json index c6f7fc92..b39bc9cf 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "extend-expect.js" ], "dependencies": { - "jest-matcher-utils": "^22.4.3", + "jest-dom": "^1.0.0", "pretty-format": "^22.4.3", "mutationobserver-shim": "^0.3.2", "wait-for-expect": "^0.4.0" diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index e84d6f8b..b4de32cd 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -1,4 +1,4 @@ -import '../extend-expect' +import 'jest-dom/extend-expect' import {render} from './helpers/test-utils' test('query can return null', () => { diff --git a/src/__tests__/example.js b/src/__tests__/example.js index 90ae13a4..09c5226a 100644 --- a/src/__tests__/example.js +++ b/src/__tests__/example.js @@ -1,7 +1,7 @@ // query utilities: import {getByLabelText, getByText, getByTestId, queryByTestId, wait} from '../' // adds special assertions like toHaveTextContent -import '../extend-expect' +import 'jest-dom/extend-expect' function getExampleDOM() { // This is just a raw example of setting up some DOM diff --git a/src/__tests__/wait-for-element.js b/src/__tests__/wait-for-element.js index 90a6ab90..11981835 100644 --- a/src/__tests__/wait-for-element.js +++ b/src/__tests__/wait-for-element.js @@ -1,6 +1,6 @@ import {waitForElement, wait} from '../' // adds special assertions like toBeInTheDOM -import '../extend-expect' +import 'jest-dom/extend-expect' import {render} from './helpers/test-utils' async function skipSomeTime(delayMs) { diff --git a/src/extend-expect.js b/src/extend-expect.js deleted file mode 100644 index a18d54f3..00000000 --- a/src/extend-expect.js +++ /dev/null @@ -1,9 +0,0 @@ -import extensions from './jest-extensions' - -const { - toBeInTheDOM, - toHaveTextContent, - toHaveAttribute, - toHaveClass, -} = extensions -expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute, toHaveClass}) diff --git a/src/jest-extensions.js b/src/jest-extensions.js deleted file mode 100644 index a44e728d..00000000 --- a/src/jest-extensions.js +++ /dev/null @@ -1,169 +0,0 @@ -import { - matcherHint, - printExpected, - stringify, - RECEIVED_COLOR as receivedColor, - EXPECTED_COLOR as expectedColor, -} from 'jest-matcher-utils' -import {matches} from './matches' - -function getDisplayName(subject) { - if (subject && subject.constructor) { - return subject.constructor.name - } else { - return typeof subject - } -} - -function checkHtmlElement(htmlElement) { - if (!(htmlElement instanceof HTMLElement)) { - throw new Error( - `The given subject is a ${getDisplayName( - htmlElement, - )}, not an HTMLElement`, - ) - } -} - -function getMessage( - matcher, - expectedLabel, - expectedValue, - receivedLabel, - receivedValue, -) { - return [ - `${matcher}\n`, - `${expectedLabel}:\n ${expectedColor(expectedValue)}`, - `${receivedLabel}:\n ${receivedColor(receivedValue)}`, - ].join('\n') -} - -function printAttribute(name, value) { - return value === undefined ? name : `${name}=${stringify(value)}` -} - -function getAttributeComment(name, value) { - return value === undefined - ? `element.hasAttribute(${stringify(name)})` - : `element.getAttribute(${stringify(name)}) === ${stringify(value)}` -} - -function splitClassNames(str) { - if (!str) { - return [] - } - return str.split(/\s+/).filter(s => s.length > 0) -} - -function isSubset(subset, superset) { - return subset.every(item => superset.includes(item)) -} - -const extensions = { - toBeInTheDOM(received) { - if (received) { - checkHtmlElement(received) - } - return { - pass: !!received, - message: () => { - const to = this.isNot ? 'not to' : 'to' - return getMessage( - matcherHint( - `${this.isNot ? '.not' : ''}.toBeInTheDOM`, - 'element', - '', - ), - 'Expected', - `element ${to} be present`, - 'Received', - received, - ) - }, - } - }, - - toHaveTextContent(htmlElement, checkWith) { - checkHtmlElement(htmlElement) - const textContent = htmlElement.textContent - return { - pass: matches(textContent, htmlElement, checkWith), - message: () => { - const to = this.isNot ? 'not to' : 'to' - return getMessage( - matcherHint( - `${this.isNot ? '.not' : ''}.toHaveTextContent`, - 'element', - '', - ), - `Expected element ${to} have text content`, - checkWith, - 'Received', - textContent, - ) - }, - } - }, - - toHaveAttribute(htmlElement, name, expectedValue) { - checkHtmlElement(htmlElement) - const isExpectedValuePresent = expectedValue !== undefined - const hasAttribute = htmlElement.hasAttribute(name) - const receivedValue = htmlElement.getAttribute(name) - return { - pass: isExpectedValuePresent - ? hasAttribute && receivedValue === expectedValue - : hasAttribute, - message: () => { - const to = this.isNot ? 'not to' : 'to' - const receivedAttribute = hasAttribute - ? printAttribute(name, receivedValue) - : null - const matcher = matcherHint( - `${this.isNot ? '.not' : ''}.toHaveAttribute`, - 'element', - printExpected(name), - { - secondArgument: isExpectedValuePresent - ? printExpected(expectedValue) - : undefined, - comment: getAttributeComment(name, expectedValue), - }, - ) - return getMessage( - matcher, - `Expected the element ${to} have attribute`, - printAttribute(name, expectedValue), - 'Received', - receivedAttribute, - ) - }, - } - }, - - toHaveClass(htmlElement, expectedClassNames) { - checkHtmlElement(htmlElement) - const received = splitClassNames(htmlElement.getAttribute('class')) - const expected = splitClassNames(expectedClassNames) - return { - pass: isSubset(expected, received), - message: () => { - const to = this.isNot ? 'not to' : 'to' - return getMessage( - matcherHint( - `${this.isNot ? '.not' : ''}.toHaveClass`, - 'element', - printExpected(expected.join(' ')), - ), - `Expected the element ${to} have class`, - expected.join(' '), - 'Received', - received.join(' '), - ) - }, - } - }, -} - -export default extensions