Skip to content

Commit

Permalink
feat(matchers): add toHaveAttribute custom matcher (closes #43) (#44)
Browse files Browse the repository at this point in the history
* Add toHaveAttribute custom matcher

* Add gnapse as contributor

* Generate messages more consistent with jest's other matchers

* Add comment to matcher hint

* Add tests to cover toHaveAttribute custom matcher
  • Loading branch information
gnapse authored and Kent C. Dodds committed Apr 5, 2018
1 parent 34c974d commit 7267acd
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 25 deletions.
11 changes: 11 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@
"contributions": [
"code"
]
},
{
"login": "gnapse",
"name": "Ernesto García",
"avatar_url": "https://avatars0.githubusercontent.com/u/15199?v=4",
"profile": "http://gnapse.github.io",
"contributions": [
"question",
"code",
"doc"
]
}
]
}
56 changes: 41 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
[![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]

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

Expand Down Expand Up @@ -82,7 +82,8 @@ facilitate testing implementation details). Read more about this in
* [Custom Jest Matchers](#custom-jest-matchers)
* [`toBeInTheDOM`](#tobeinthedom)
* [`toHaveTextContent`](#tohavetextcontent)
* [Custom Jest Matchers - Typescript](#custom-jest-matchers-typescript)
* [`toHaveAttribute`](#tohaveattribute)
* [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript)
* [`TextMatch`](#textmatch)
* [`query` APIs](#query-apis)
* [Examples](#examples)
Expand Down Expand Up @@ -138,6 +139,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
expect(getByTestId('ok-button')).toHaveAttribute('disabled')
// snapshots work great with regular DOM nodes!
expect(container.firstChild).toMatchSnapshot()
})
Expand Down Expand Up @@ -347,33 +349,55 @@ expect(getByTestId('count-value')).toHaveTextContent('2')
expect(getByTestId('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 'react-testing-library/extend-expect'

// ...
const {getByTestId} = render(
<button data-testid="ok-button" type="submit" disabled>
OK
</button>,
)
expect(getByTestId('ok-button')).toHaveAttribute('disabled')
expect(getByTestId('ok-button')).toHaveAttribute('type', 'submit')
expect(getByTestId('ok-button')).not.toHaveAttribute('type', 'button')
// ...
```
### Custom Jest Matchers - Typescript
When you use custom Jest Matchers with Typescript, you will need to extend the type signature of `jest.Matchers<void>`, then cast the result of `expect` accordingly. Here's a handy usage example:
When you use custom Jest Matchers with Typescript, you will need to extend the type signature of `jest.Matchers<void>`, then cast the result of `expect` accordingly. Here's a handy usage example:
```typescript
// this adds custom expect matchers
import 'react-testing-library/extend-expect';
import 'react-testing-library/extend-expect'
interface ExtendedMatchers extends jest.Matchers<void> {
toHaveTextContent: (htmlElement: string) => object;
toBeInTheDOM: () => void;
toHaveTextContent: (htmlElement: string) => object
toBeInTheDOM: () => void
}
test('renders the tooltip as expected', async () => {
const {
// getByLabelText,
getByText,
// getByTestId,
container
} = render(<Tooltip label="hello world">Child</Tooltip>);
container,
} = render(<Tooltip label="hello world">Child</Tooltip>)
// tests rendering of the child
getByText('Child');
getByText('Child')
// tests rendering of tooltip label
(expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent(
'hello world'
);
;(expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent(
'hello world',
)
// snapshots work great with regular DOM nodes!
expect(container.firstChild).toMatchSnapshot();
});
expect(container.firstChild).toMatchSnapshot()
})
```
## `TextMatch`
Expand Down Expand Up @@ -715,10 +739,12 @@ light-weight, simple, and understandable.
Thanks goes to these people ([emoji key][emojis]):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- 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://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") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Tests") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") |
| [<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") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Tests") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💬](#question-gnapse "Answering Questions") [💻](https://github.com/kentcdodds/react-testing-library/commits?author=gnapse "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=gnapse "Documentation") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors][all-contributors] specification.
Expand Down
30 changes: 30 additions & 0 deletions src/__tests__/element-queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,34 @@ test('using jest helpers to assert element states', () => {
).toThrowError()
})

test('using jest helpers to check element attributes', () => {
const {queryByTestId} = render(
<button data-testid="ok-button" type="submit" disabled>
OK
</button>,
)

expect(queryByTestId('ok-button')).toHaveAttribute('disabled')
expect(queryByTestId('ok-button')).toHaveAttribute('type')
expect(queryByTestId('ok-button')).not.toHaveAttribute('class')
expect(queryByTestId('ok-button')).toHaveAttribute('type', 'submit')
expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'button')

expect(() =>
expect(queryByTestId('ok-button')).not.toHaveAttribute('disabled'),
).toThrowError()
expect(() =>
expect(queryByTestId('ok-button')).not.toHaveAttribute('type'),
).toThrowError()
expect(() =>
expect(queryByTestId('ok-button')).toHaveAttribute('class'),
).toThrowError()
expect(() =>
expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'submit'),
).toThrowError()
expect(() =>
expect(queryByTestId('ok-button')).toHaveAttribute('type', 'button'),
).toThrowError()
})

/* eslint jsx-a11y/label-has-for:0 */
4 changes: 2 additions & 2 deletions src/extend-expect.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import extensions from './jest-extensions'

const {toBeInTheDOM, toHaveTextContent} = extensions
expect.extend({toBeInTheDOM, toHaveTextContent})
const {toBeInTheDOM, toHaveTextContent, toHaveAttribute} = extensions
expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute})
73 changes: 65 additions & 8 deletions src/jest-extensions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import {matcherHint, printReceived, printExpected} from 'jest-matcher-utils' //eslint-disable-line import/no-extraneous-dependencies
//eslint-disable-next-line import/no-extraneous-dependencies
import {
matcherHint,
printReceived,
printExpected,
stringify,
RECEIVED_COLOR as receivedColor,
EXPECTED_COLOR as expectedColor,
} from 'jest-matcher-utils'
import {matches} from './utils'

function getDisplayName(subject) {
Expand All @@ -9,10 +17,30 @@ function getDisplayName(subject) {
}
}

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

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

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)}`
}

const extensions = {
toBeInTheDOM(received) {
getDisplayName(received)
Expand Down Expand Up @@ -42,13 +70,7 @@ const extensions = {
},

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

checkHtmlElement(htmlElement)
const textContent = htmlElement.textContent
const pass = matches(textContent, htmlElement, checkWith)
if (pass) {
Expand All @@ -75,6 +97,41 @@ const extensions = {
}
}
},

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 = receivedColor(
hasAttribute
? printAttribute(name, receivedValue)
: 'attribute was not found',
)
const expectedMsg = `Expected the element ${to} have attribute:\n ${expectedColor(
printAttribute(name, expectedValue),
)}`
const matcher = matcherHint(
`${this.isNot ? '.not' : ''}.toHaveAttribute`,
'element',
printExpected(name),
{
secondArgument: isExpectedValuePresent
? printExpected(expectedValue)
: undefined,
comment: getAttributeComment(name, expectedValue),
},
)
return `${matcher}\n\n${expectedMsg}\nReceived:\n ${receivedAttribute}`
},
}
},
}

export default extensions

0 comments on commit 7267acd

Please sign in to comment.