Skip to content

Commit 7267acd

Browse files
gnapseKent C. Dodds
authored and
Kent C. Dodds
committed
feat(matchers): add toHaveAttribute custom matcher (closes #43) (#44)
* 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
1 parent 34c974d commit 7267acd

File tree

5 files changed

+149
-25
lines changed

5 files changed

+149
-25
lines changed

.all-contributorsrc

+11
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@
126126
"contributions": [
127127
"code"
128128
]
129+
},
130+
{
131+
"login": "gnapse",
132+
"name": "Ernesto García",
133+
"avatar_url": "https://avatars0.githubusercontent.com/u/15199?v=4",
134+
"profile": "http://gnapse.github.io",
135+
"contributions": [
136+
"question",
137+
"code",
138+
"doc"
139+
]
129140
}
130141
]
131142
}

README.md

+41-15
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
[![downloads][downloads-badge]][npmtrends]
1717
[![MIT License][license-badge]][license]
1818

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

@@ -82,7 +82,8 @@ facilitate testing implementation details). Read more about this in
8282
* [Custom Jest Matchers](#custom-jest-matchers)
8383
* [`toBeInTheDOM`](#tobeinthedom)
8484
* [`toHaveTextContent`](#tohavetextcontent)
85-
* [Custom Jest Matchers - Typescript](#custom-jest-matchers-typescript)
85+
* [`toHaveAttribute`](#tohaveattribute)
86+
* [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript)
8687
* [`TextMatch`](#textmatch)
8788
* [`query` APIs](#query-apis)
8889
* [Examples](#examples)
@@ -138,6 +139,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
138139
expect(axiosMock.get).toHaveBeenCalledTimes(1)
139140
expect(axiosMock.get).toHaveBeenCalledWith(url)
140141
expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
142+
expect(getByTestId('ok-button')).toHaveAttribute('disabled')
141143
// snapshots work great with regular DOM nodes!
142144
expect(container.firstChild).toMatchSnapshot()
143145
})
@@ -347,33 +349,55 @@ expect(getByTestId('count-value')).toHaveTextContent('2')
347349
expect(getByTestId('count-value')).not.toHaveTextContent('21')
348350
// ...
349351
```
352+
353+
### `toHaveAttribute`
354+
355+
This allows you to check wether the given element has an attribute or not. You
356+
can also optionally check that the attribute has a specific expected value.
357+
358+
```javascript
359+
// add the custom expect matchers
360+
import 'react-testing-library/extend-expect'
361+
362+
// ...
363+
const {getByTestId} = render(
364+
<button data-testid="ok-button" type="submit" disabled>
365+
OK
366+
</button>,
367+
)
368+
expect(getByTestId('ok-button')).toHaveAttribute('disabled')
369+
expect(getByTestId('ok-button')).toHaveAttribute('type', 'submit')
370+
expect(getByTestId('ok-button')).not.toHaveAttribute('type', 'button')
371+
// ...
372+
```
373+
350374
### Custom Jest Matchers - Typescript
351375
352-
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:
376+
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:
353377
354378
```typescript
355379
// this adds custom expect matchers
356-
import 'react-testing-library/extend-expect';
380+
import 'react-testing-library/extend-expect'
357381
interface ExtendedMatchers extends jest.Matchers<void> {
358-
toHaveTextContent: (htmlElement: string) => object;
359-
toBeInTheDOM: () => void;
382+
toHaveTextContent: (htmlElement: string) => object
383+
toBeInTheDOM: () => void
360384
}
361385
test('renders the tooltip as expected', async () => {
362386
const {
363387
// getByLabelText,
364388
getByText,
365389
// getByTestId,
366-
container
367-
} = render(<Tooltip label="hello world">Child</Tooltip>);
390+
container,
391+
} = render(<Tooltip label="hello world">Child</Tooltip>)
368392
// tests rendering of the child
369-
getByText('Child');
393+
getByText('Child')
370394
// tests rendering of tooltip label
371-
(expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent(
372-
'hello world'
373-
);
395+
;(expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent(
396+
'hello world',
397+
)
374398
// snapshots work great with regular DOM nodes!
375-
expect(container.firstChild).toMatchSnapshot();
376-
});
399+
expect(container.firstChild).toMatchSnapshot()
400+
})
377401
```
378402
379403
## `TextMatch`
@@ -715,10 +739,12 @@ light-weight, simple, and understandable.
715739
Thanks goes to these people ([emoji key][emojis]):
716740
717741
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
742+
718743
<!-- prettier-ignore -->
719744
| [<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") |
720745
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
721-
| [<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") |
746+
| [<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") |
747+
722748
<!-- ALL-CONTRIBUTORS-LIST:END -->
723749
724750
This project follows the [all-contributors][all-contributors] specification.

src/__tests__/element-queries.js

+30
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,34 @@ test('using jest helpers to assert element states', () => {
117117
).toThrowError()
118118
})
119119

120+
test('using jest helpers to check element attributes', () => {
121+
const {queryByTestId} = render(
122+
<button data-testid="ok-button" type="submit" disabled>
123+
OK
124+
</button>,
125+
)
126+
127+
expect(queryByTestId('ok-button')).toHaveAttribute('disabled')
128+
expect(queryByTestId('ok-button')).toHaveAttribute('type')
129+
expect(queryByTestId('ok-button')).not.toHaveAttribute('class')
130+
expect(queryByTestId('ok-button')).toHaveAttribute('type', 'submit')
131+
expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'button')
132+
133+
expect(() =>
134+
expect(queryByTestId('ok-button')).not.toHaveAttribute('disabled'),
135+
).toThrowError()
136+
expect(() =>
137+
expect(queryByTestId('ok-button')).not.toHaveAttribute('type'),
138+
).toThrowError()
139+
expect(() =>
140+
expect(queryByTestId('ok-button')).toHaveAttribute('class'),
141+
).toThrowError()
142+
expect(() =>
143+
expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'submit'),
144+
).toThrowError()
145+
expect(() =>
146+
expect(queryByTestId('ok-button')).toHaveAttribute('type', 'button'),
147+
).toThrowError()
148+
})
149+
120150
/* eslint jsx-a11y/label-has-for:0 */

src/extend-expect.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import extensions from './jest-extensions'
22

3-
const {toBeInTheDOM, toHaveTextContent} = extensions
4-
expect.extend({toBeInTheDOM, toHaveTextContent})
3+
const {toBeInTheDOM, toHaveTextContent, toHaveAttribute} = extensions
4+
expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute})

src/jest-extensions.js

+65-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import {matcherHint, printReceived, printExpected} from 'jest-matcher-utils' //eslint-disable-line import/no-extraneous-dependencies
1+
//eslint-disable-next-line import/no-extraneous-dependencies
2+
import {
3+
matcherHint,
4+
printReceived,
5+
printExpected,
6+
stringify,
7+
RECEIVED_COLOR as receivedColor,
8+
EXPECTED_COLOR as expectedColor,
9+
} from 'jest-matcher-utils'
210
import {matches} from './utils'
311

412
function getDisplayName(subject) {
@@ -9,10 +17,30 @@ function getDisplayName(subject) {
917
}
1018
}
1119

20+
function checkHtmlElement(htmlElement) {
21+
if (!(htmlElement instanceof HTMLElement)) {
22+
throw new Error(
23+
`The given subject is a ${getDisplayName(
24+
htmlElement,
25+
)}, not an HTMLElement`,
26+
)
27+
}
28+
}
29+
1230
const assertMessage = (assertionName, message, received, expected) =>
1331
`${matcherHint(`${assertionName}`, 'received', '')} \n${message}: ` +
1432
`${printExpected(expected)} \nReceived: ${printReceived(received)}`
1533

34+
function printAttribute(name, value) {
35+
return value === undefined ? name : `${name}=${stringify(value)}`
36+
}
37+
38+
function getAttributeComment(name, value) {
39+
return value === undefined
40+
? `element.hasAttribute(${stringify(name)})`
41+
: `element.getAttribute(${stringify(name)}) === ${stringify(value)}`
42+
}
43+
1644
const extensions = {
1745
toBeInTheDOM(received) {
1846
getDisplayName(received)
@@ -42,13 +70,7 @@ const extensions = {
4270
},
4371

4472
toHaveTextContent(htmlElement, checkWith) {
45-
if (!(htmlElement instanceof HTMLElement))
46-
throw new Error(
47-
`The given subject is a ${getDisplayName(
48-
htmlElement,
49-
)}, not an HTMLElement`,
50-
)
51-
73+
checkHtmlElement(htmlElement)
5274
const textContent = htmlElement.textContent
5375
const pass = matches(textContent, htmlElement, checkWith)
5476
if (pass) {
@@ -75,6 +97,41 @@ const extensions = {
7597
}
7698
}
7799
},
100+
101+
toHaveAttribute(htmlElement, name, expectedValue) {
102+
checkHtmlElement(htmlElement)
103+
const isExpectedValuePresent = expectedValue !== undefined
104+
const hasAttribute = htmlElement.hasAttribute(name)
105+
const receivedValue = htmlElement.getAttribute(name)
106+
return {
107+
pass: isExpectedValuePresent
108+
? hasAttribute && receivedValue === expectedValue
109+
: hasAttribute,
110+
message: () => {
111+
const to = this.isNot ? 'not to' : 'to'
112+
const receivedAttribute = receivedColor(
113+
hasAttribute
114+
? printAttribute(name, receivedValue)
115+
: 'attribute was not found',
116+
)
117+
const expectedMsg = `Expected the element ${to} have attribute:\n ${expectedColor(
118+
printAttribute(name, expectedValue),
119+
)}`
120+
const matcher = matcherHint(
121+
`${this.isNot ? '.not' : ''}.toHaveAttribute`,
122+
'element',
123+
printExpected(name),
124+
{
125+
secondArgument: isExpectedValuePresent
126+
? printExpected(expectedValue)
127+
: undefined,
128+
comment: getAttributeComment(name, expectedValue),
129+
},
130+
)
131+
return `${matcher}\n\n${expectedMsg}\nReceived:\n ${receivedAttribute}`
132+
},
133+
}
134+
},
78135
}
79136

80137
export default extensions

0 commit comments

Comments
 (0)