Skip to content

Commit 75d12c4

Browse files
gnapseKent C. Dodds
authored and
Kent C. Dodds
committed
feat(matchers): add toHaveClass custom matcher (closes testing-library#2) (testing-library#4)
* Add toHaveClass custom matcher * Add documentation in the README * Handle the case where an element has no class
1 parent 075528d commit 75d12c4

File tree

4 files changed

+104
-2
lines changed

4 files changed

+104
-2
lines changed

README.md

+20
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ when a real user uses it.
8181
* [`toBeInTheDOM`](#tobeinthedom)
8282
* [`toHaveTextContent`](#tohavetextcontent)
8383
* [`toHaveAttribute`](#tohaveattribute)
84+
* [`toHaveClass`](#tohaveclass)
8485
* [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript)
8586
* [`TextMatch`](#textmatch)
8687
* [`query` APIs](#query-apis)
@@ -364,6 +365,25 @@ expect(getByTestId(container, 'ok-button')).not.toHaveAttribute(
364365
// ...
365366
```
366367

368+
### `toHaveClass`
369+
370+
This allows you to check wether the given element has certain classes within its
371+
`class` attribute.
372+
373+
```javascript
374+
// add the custom expect matchers
375+
import 'dom-testing-library/extend-expect'
376+
377+
// ...
378+
// <button data-testid="delete-button" class="btn extra btn-danger">
379+
// Delete item
380+
// </button>
381+
expect(getByTestId(container, 'delete-button')).toHaveClass('extra')
382+
expect(getByTestId(container, 'delete-button')).toHaveClass('btn-danger btn')
383+
expect(getByTestId(container, 'delete-button')).not.toHaveClass('btn-link')
384+
// ...
385+
```
386+
367387
### Custom Jest Matchers - Typescript
368388

369389
When you use custom Jest Matchers with Typescript, you will need to extend the

src/__tests__/element-queries.js

+43
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,47 @@ test('using jest helpers to check element attributes', () => {
150150
).toThrowError()
151151
})
152152

153+
test('using jest helpers to check element class names', () => {
154+
const {getByTestId} = render(`
155+
<div>
156+
<button data-testid="delete-button" class="btn extra btn-danger">
157+
Delete item
158+
</button>
159+
<button data-testid="cancel-button">
160+
Cancel
161+
</button>
162+
</div>
163+
`)
164+
165+
expect(getByTestId('delete-button')).toHaveClass('btn')
166+
expect(getByTestId('delete-button')).toHaveClass('btn-danger')
167+
expect(getByTestId('delete-button')).toHaveClass('extra')
168+
expect(getByTestId('delete-button')).not.toHaveClass('xtra')
169+
expect(getByTestId('delete-button')).toHaveClass('btn btn-danger')
170+
expect(getByTestId('delete-button')).not.toHaveClass('btn-link')
171+
expect(getByTestId('cancel-button')).not.toHaveClass('btn-danger')
172+
173+
expect(() =>
174+
expect(getByTestId('delete-button')).not.toHaveClass('btn'),
175+
).toThrowError()
176+
expect(() =>
177+
expect(getByTestId('delete-button')).not.toHaveClass('btn-danger'),
178+
).toThrowError()
179+
expect(() =>
180+
expect(getByTestId('delete-button')).not.toHaveClass('extra'),
181+
).toThrowError()
182+
expect(() =>
183+
expect(getByTestId('delete-button')).toHaveClass('xtra'),
184+
).toThrowError()
185+
expect(() =>
186+
expect(getByTestId('delete-button')).not.toHaveClass('btn btn-danger'),
187+
).toThrowError()
188+
expect(() =>
189+
expect(getByTestId('delete-button')).toHaveClass('btn-link'),
190+
).toThrowError()
191+
expect(() =>
192+
expect(getByTestId('cancel-button')).toHaveClass('btn-danger'),
193+
).toThrowError()
194+
})
195+
153196
/* eslint jsx-a11y/label-has-for:0 */

src/extend-expect.js

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

3-
const {toBeInTheDOM, toHaveTextContent, toHaveAttribute} = extensions
4-
expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute})
3+
const {
4+
toBeInTheDOM,
5+
toHaveTextContent,
6+
toHaveAttribute,
7+
toHaveClass,
8+
} = extensions
9+
expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute, toHaveClass})

src/jest-extensions.js

+34
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ function getAttributeComment(name, value) {
4949
: `element.getAttribute(${stringify(name)}) === ${stringify(value)}`
5050
}
5151

52+
function splitClassNames(str) {
53+
if (!str) {
54+
return []
55+
}
56+
return str.split(/\s+/).filter(s => s.length > 0)
57+
}
58+
59+
function isSubset(subset, superset) {
60+
return subset.every(item => superset.includes(item))
61+
}
62+
5263
const extensions = {
5364
toBeInTheDOM(received) {
5465
if (received) {
@@ -130,6 +141,29 @@ const extensions = {
130141
},
131142
}
132143
},
144+
145+
toHaveClass(htmlElement, expectedClassNames) {
146+
checkHtmlElement(htmlElement)
147+
const received = splitClassNames(htmlElement.getAttribute('class'))
148+
const expected = splitClassNames(expectedClassNames)
149+
return {
150+
pass: isSubset(expected, received),
151+
message: () => {
152+
const to = this.isNot ? 'not to' : 'to'
153+
return getMessage(
154+
matcherHint(
155+
`${this.isNot ? '.not' : ''}.toHaveClass`,
156+
'element',
157+
printExpected(expected.join(' ')),
158+
),
159+
`Expected the element ${to} have class`,
160+
expected.join(' '),
161+
'Received',
162+
received.join(' '),
163+
)
164+
},
165+
}
166+
},
133167
}
134168

135169
export default extensions

0 commit comments

Comments
 (0)