Skip to content

Commit

Permalink
feat(matchers): add custom jest matchers (#13)
Browse files Browse the repository at this point in the history
* Improving API's for testing.

* Improving Apis

* Adding all contributions

* Fixing wrong url

* Fixing review comments & making colorful assertions :)

* Removing unwanted changes

* Fixing review comments

* removing unwanted comments

* Adding test cases for the coverage

* removing commented code and making few changes to the contribution file

* Updating the readme

* Making line break changes

* Update README.md
  • Loading branch information
antsmartian authored and Kent C. Dodds committed Mar 28, 2018
1 parent 50aa1bc commit 18104fe
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 15 deletions.
11 changes: 11 additions & 0 deletions .all-contributorsrc
Expand Up @@ -77,6 +77,17 @@
"platform"
]
},
{
"login": "antoaravinth",
"name": "Anto Aravinth",
"avatar_url": "https://avatars1.githubusercontent.com/u/1241511?s=460&v=4",
"profile": "https://github.com/antoaravinth",
"contributions": [
"code",
"test",
"doc"
]
},
{
"login": "JonahMoses",
"name": "Jonah Moses",
Expand Down
43 changes: 41 additions & 2 deletions README.md
Expand Up @@ -16,7 +16,7 @@
[![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]

[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors)
[![PRs Welcome][prs-badge]][prs]
[![Code of Conduct][coc-badge]][coc]

Expand Down Expand Up @@ -79,6 +79,7 @@ facilitate testing implementation details). Read more about this in
* [`Simulate`](#simulate)
* [`flushPromises`](#flushpromises)
* [`render`](#render)
* [Custom Jest Matchers](#custom-jest-matchers)
* [`TextMatch`](#textmatch)
* [`query` APIs](#query-apis)
* [Examples](#examples)
Expand Down Expand Up @@ -250,6 +251,44 @@ const usernameInputElement = getByTestId('username-input')
> Learn more about `data-testid`s from the blog post
> ["Making your UI tests resilient to change"][data-testid-blog-post]
## 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.

```javascript
// add the custom expect matchers
import 'react-testing-library/extend-expect'

// ...
const {queryByTestId} = render(<span data-testid="count-value">2</span>)
expect(queryByTestId('count-value')).toBeInTheDOM()
expect(queryByTestId('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 'react-testing-library/extend-expect'

// ...
const {getByTestId} = render(<span data-testid="count-value">2</span>)
expect(getByTestId('count-value')).toHaveTextContent('2')
expect(getByTestId('count-value')).not.toHaveTextContent('21')
// ...
```

## `TextMatch`

Several APIs accept a `TextMatch` which can be a `string`, `regex` or a
Expand Down Expand Up @@ -595,7 +634,7 @@ Thanks goes to these people ([emoji key][emojis]):
<!-- prettier-ignore -->
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[馃捇](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [馃摉](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [馃殗](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [鈿狅笍](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[馃摉](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[馃捇](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Pawe艂 Miko艂ajczyk</b></sub>](https://github.com/Miklet)<br />[馃捇](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro 脩谩帽ez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[馃摉](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1402095?v=4" width="100px;"/><br /><sub><b>Matt Parrish</b></sub>](https://github.com/pbomb)<br />[馃悰](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Apbomb "Bug reports") [馃捇](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Code") [馃摉](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Documentation") [鈿狅笍](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Tests") | [<img src="https://avatars1.githubusercontent.com/u/1288694?v=4" width="100px;"/><br /><sub><b>Justin Hall</b></sub>](https://github.com/wKovacs64)<br />[馃摝](#platform-wKovacs64 "Packaging/porting to new platform") |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[馃摉](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") |
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[馃捇](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Code") [鈿狅笍](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Tests") [馃摉](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[馃摉](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") |

<!-- ALL-CONTRIBUTORS-LIST:END -->

Expand Down
1 change: 1 addition & 0 deletions extend-expect.js
@@ -0,0 +1 @@
require('./dist/extend-expect')
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -45,7 +45,8 @@
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
"rules": {
"react/prop-types": "off"
"react/prop-types": "off",
"import/no-unassigned-import": "off"
}
},
"eslintIgnore": [
Expand All @@ -61,4 +62,4 @@
"url": "https://github.com/kentcdodds/react-testing-library/issues"
},
"homepage": "https://github.com/kentcdodds/react-testing-library#readme"
}
}
14 changes: 14 additions & 0 deletions src/__tests__/element-queries.js
@@ -1,5 +1,6 @@
import React from 'react'
import {render} from '../'
import '../extend-expect'

test('query can return null', () => {
const {
Expand Down Expand Up @@ -66,4 +67,17 @@ test('totally empty label', () => {
expect(() => getByLabelText('')).toThrowErrorMatchingSnapshot()
})

test('using jest helpers to assert element states', () => {
const {queryByTestId} = render(<span data-testid="count-value">2</span>)

// other ways to assert your test cases, but you don't need all of them.
expect(queryByTestId('count-value')).toBeInTheDOM()
expect(queryByTestId('count-value1')).not.toBeInTheDOM()
expect(queryByTestId('count-value')).toHaveTextContent('2')
expect(queryByTestId('count-value')).not.toHaveTextContent('21')
expect(() =>
expect(queryByTestId('count-value2')).toHaveTextContent('2'),
).toThrowError()
})

/* eslint jsx-a11y/label-has-for:0 */
2 changes: 1 addition & 1 deletion src/__tests__/fetch.js
Expand Up @@ -35,7 +35,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
}),
)
const url = '/greeting'
const {getByText, container} = render(<Fetch url={url} />)
const {container, getByText} = render(<Fetch url={url} />)

// Act
Simulate.click(getByText('Fetch'))
Expand Down
7 changes: 7 additions & 0 deletions src/extend-expect.js
@@ -0,0 +1,7 @@
import expect from 'expect' //eslint-disable-line import/no-extraneous-dependencies
import extensions from './jest-extensions'

const {toBeInTheDOM, toHaveTextContent} = extensions
expect.extend({toBeInTheDOM, toHaveTextContent})

export default expect
78 changes: 78 additions & 0 deletions src/jest-extensions.js
@@ -0,0 +1,78 @@
import {matcherHint, printReceived, printExpected} from 'jest-matcher-utils' //eslint-disable-line import/no-extraneous-dependencies
import {matches} from './utils'

function getDisplayName(subject) {
if (subject && subject.constructor) {
return subject.constructor.name
} else {
return typeof subject
}
}

const assertMessage = (assertionName, message, received, expected) =>
`${matcherHint(`${assertionName}`, 'received', '')} \n${message}: ` +
`${printExpected(expected)} \nReceived: ${printReceived(received)}`

const extensions = {
toBeInTheDOM(received) {
getDisplayName(received)
if (received) {
return {
message:
`${matcherHint(
'.not.toBeInTheDOM',
'received',
'',
)} Expected the element not to be present` +
`\nReceived : ${printReceived(received)}`,
pass: true,
}
} else {
return {
message:
`${matcherHint(
'.toBeInTheDOM',
'received',
'',
)} Expected the element to be present` +
`\nReceived : ${printReceived(received)}`,
pass: false,
}
}
},

toHaveTextContent(htmlElement, checkWith) {
if (!(htmlElement instanceof HTMLElement))
throw new Error(
`The given subject is a ${getDisplayName(
htmlElement,
)}, not an HTMLElement`,
)

const textContent = htmlElement.textContent
const pass = matches(textContent, htmlElement, checkWith)
if (pass) {
return {
message: assertMessage(
'.not.toHaveTextContent',
'Expected value not equals to',
htmlElement,
checkWith,
),
pass: true,
}
} else {
return {
message: assertMessage(
'.toHaveTextContent',
'Expected value equals to',
htmlElement,
checkWith,
),
pass: false,
}
}
},
}

export default extensions
12 changes: 2 additions & 10 deletions src/queries.js
@@ -1,3 +1,5 @@
import {matches} from './utils'

// Here are the queries for the library.
// The queries here should only be things that are accessible to both users who are using a screen reader
// and those who are not using a screen reader (with the exception of the data-testid attribute query).
Expand Down Expand Up @@ -69,16 +71,6 @@ function getText(node) {
.join(' ')
}

function matches(textToMatch, node, matcher) {
if (typeof matcher === 'string') {
return textToMatch.toLowerCase().includes(matcher.toLowerCase())
} else if (typeof matcher === 'function') {
return matcher(textToMatch, node)
} else {
return matcher.test(textToMatch)
}
}

// getters
// the reason we're not dynamically generating these functions that look so similar:
// 1. The error messages are specific to each one and depend on arguments
Expand Down
10 changes: 10 additions & 0 deletions src/utils.js
@@ -0,0 +1,10 @@
//eslint-disable-next-line import/prefer-default-export
export function matches(textToMatch, node, matcher) {
if (typeof matcher === 'string') {
return textToMatch.toLowerCase().includes(matcher.toLowerCase())
} else if (typeof matcher === 'function') {
return matcher(textToMatch, node)
} else {
return matcher.test(textToMatch)
}
}

0 comments on commit 18104fe

Please sign in to comment.