Skip to content

Commit

Permalink
feat(matchers): add toHaveClass custom matcher (closes #2) (#4)
Browse files Browse the repository at this point in the history
* Add toHaveClass custom matcher

* Add documentation in the README

* Handle the case where an element has no class
  • Loading branch information
gnapse authored and Kent C. Dodds committed Apr 7, 2018
1 parent 075528d commit 75d12c4
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 2 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ when a real user uses it.
* [`toBeInTheDOM`](#tobeinthedom)
* [`toHaveTextContent`](#tohavetextcontent)
* [`toHaveAttribute`](#tohaveattribute)
* [`toHaveClass`](#tohaveclass)
* [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript)
* [`TextMatch`](#textmatch)
* [`query` APIs](#query-apis)
Expand Down Expand Up @@ -364,6 +365,25 @@ expect(getByTestId(container, 'ok-button')).not.toHaveAttribute(
// ...
```

### `toHaveClass`

This allows you to check wether the given element has certain classes within its
`class` attribute.

```javascript
// add the custom expect matchers
import 'dom-testing-library/extend-expect'
// ...
// <button data-testid="delete-button" class="btn extra btn-danger">
// Delete item
// </button>
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

When you use custom Jest Matchers with Typescript, you will need to extend the
Expand Down
43 changes: 43 additions & 0 deletions src/__tests__/element-queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,47 @@ test('using jest helpers to check element attributes', () => {
).toThrowError()
})

test('using jest helpers to check element class names', () => {
const {getByTestId} = render(`
<div>
<button data-testid="delete-button" class="btn extra btn-danger">
Delete item
</button>
<button data-testid="cancel-button">
Cancel
</button>
</div>
`)

expect(getByTestId('delete-button')).toHaveClass('btn')
expect(getByTestId('delete-button')).toHaveClass('btn-danger')
expect(getByTestId('delete-button')).toHaveClass('extra')
expect(getByTestId('delete-button')).not.toHaveClass('xtra')
expect(getByTestId('delete-button')).toHaveClass('btn btn-danger')
expect(getByTestId('delete-button')).not.toHaveClass('btn-link')
expect(getByTestId('cancel-button')).not.toHaveClass('btn-danger')

expect(() =>
expect(getByTestId('delete-button')).not.toHaveClass('btn'),
).toThrowError()
expect(() =>
expect(getByTestId('delete-button')).not.toHaveClass('btn-danger'),
).toThrowError()
expect(() =>
expect(getByTestId('delete-button')).not.toHaveClass('extra'),
).toThrowError()
expect(() =>
expect(getByTestId('delete-button')).toHaveClass('xtra'),
).toThrowError()
expect(() =>
expect(getByTestId('delete-button')).not.toHaveClass('btn btn-danger'),
).toThrowError()
expect(() =>
expect(getByTestId('delete-button')).toHaveClass('btn-link'),
).toThrowError()
expect(() =>
expect(getByTestId('cancel-button')).toHaveClass('btn-danger'),
).toThrowError()
})

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

const {toBeInTheDOM, toHaveTextContent, toHaveAttribute} = extensions
expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute})
const {
toBeInTheDOM,
toHaveTextContent,
toHaveAttribute,
toHaveClass,
} = extensions
expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute, toHaveClass})
34 changes: 34 additions & 0 deletions src/jest-extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ function getAttributeComment(name, value) {
: `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) {
Expand Down Expand Up @@ -130,6 +141,29 @@ const extensions = {
},
}
},

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

0 comments on commit 75d12c4

Please sign in to comment.