Skip to content

Commit

Permalink
feat: add exact mode option for toHaveClass (#176) (#217)
Browse files Browse the repository at this point in the history
* feat: add exact mode option for toHaveClass (#176)
* Update src/to-have-class.js

Co-authored-by: Ernesto García <gnapse@gmail.com>
  • Loading branch information
SergiCL and gnapse committed Mar 26, 2020
1 parent eb51c17 commit cc8721e
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 16 deletions.
7 changes: 6 additions & 1 deletion README.md
Expand Up @@ -46,6 +46,7 @@ clear to read and to maintain.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Installation](#installation)
- [Usage](#usage)
- [Custom matchers](#custom-matchers)
Expand Down Expand Up @@ -498,7 +499,7 @@ expect(button).toHaveAttribute('type', expect.not.stringContaining('but'))
### `toHaveClass`

```typescript
toHaveClass(...classNames: string[])
toHaveClass(...classNames: string[], options?: {exact: boolean})
```

This allows you to check whether the given element has certain classes within
Expand All @@ -525,6 +526,9 @@ expect(deleteButton).toHaveClass('btn-danger btn')
expect(deleteButton).toHaveClass('btn-danger', 'btn')
expect(deleteButton).not.toHaveClass('btn-link')

expect(deleteButton).toHaveClass('btn-danger extra btn', {exact: true}) // to check if the element has EXACTLY a set of classes
expect(deleteButton).not.toHaveClass('btn-danger extra', {exact: true}) // if it has more than expected it is going to fail

expect(noClasses).not.toHaveClass()
```

Expand Down Expand Up @@ -940,6 +944,7 @@ Thanks goes to these people ([emoji key][emojis]):

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

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

This project follows the [all-contributors][all-contributors] specification.
Expand Down
118 changes: 104 additions & 14 deletions src/__tests__/to-have-class.js
Expand Up @@ -2,21 +2,25 @@

import {render} from './helpers/test-utils'

const renderElementWithClasses = () =>
render(`
<div>
<button data-testid="delete-button" class="btn extra btn-danger">
Delete item
</button>
<button data-testid="cancel-button">
Cancel
</button>
<svg data-testid="svg-spinner" class="spinner clockwise">
<path />
</svg>
<div data-testid="only-one-class" class="alone"></div>
<div data-testid="no-classes"></div>
</div>
`)

test('.toHaveClass', () => {
const {queryByTestId} = render(`
<div>
<button data-testid="delete-button" class="btn extra btn-danger">
Delete item
</button>
<button data-testid="cancel-button">
Cancel
</button>
<svg data-testid="svg-spinner" class="spinner clockwise">
<path />
</svg>
<div data-testid="no-classes"></div>
</div>
`)
const {queryByTestId} = renderElementWithClasses()

expect(queryByTestId('delete-button')).toHaveClass('btn')
expect(queryByTestId('delete-button')).toHaveClass('btn-danger')
Expand Down Expand Up @@ -91,3 +95,89 @@ test('.toHaveClass', () => {
expect(queryByTestId('delete-button')).not.toHaveClass(' '),
).toThrowError(/(none)/)
})

test('.toHaveClass with exact mode option', () => {
const {queryByTestId} = renderElementWithClasses()

expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', {
exact: true,
})
expect(queryByTestId('delete-button')).not.toHaveClass('btn extra', {
exact: true,
})
expect(
queryByTestId('delete-button'),
).not.toHaveClass('btn extra btn-danger foo', {exact: true})

expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', {
exact: false,
})
expect(queryByTestId('delete-button')).toHaveClass('btn extra', {
exact: false,
})
expect(
queryByTestId('delete-button'),
).not.toHaveClass('btn extra btn-danger foo', {exact: false})

expect(queryByTestId('delete-button')).toHaveClass(
'btn',
'extra',
'btn-danger',
{exact: true},
)
expect(queryByTestId('delete-button')).not.toHaveClass('btn', 'extra', {
exact: true,
})
expect(queryByTestId('delete-button')).not.toHaveClass(
'btn',
'extra',
'btn-danger',
'foo',
{exact: true},
)

expect(queryByTestId('delete-button')).toHaveClass(
'btn',
'extra',
'btn-danger',
{exact: false},
)
expect(queryByTestId('delete-button')).toHaveClass('btn', 'extra', {
exact: false,
})
expect(queryByTestId('delete-button')).not.toHaveClass(
'btn',
'extra',
'btn-danger',
'foo',
{exact: false},
)

expect(queryByTestId('only-one-class')).toHaveClass('alone', {exact: true})
expect(queryByTestId('only-one-class')).not.toHaveClass('alone foo', {
exact: true,
})
expect(queryByTestId('only-one-class')).not.toHaveClass('alone', 'foo', {
exact: true,
})

expect(queryByTestId('only-one-class')).toHaveClass('alone', {exact: false})
expect(queryByTestId('only-one-class')).not.toHaveClass('alone foo', {
exact: false,
})
expect(queryByTestId('only-one-class')).not.toHaveClass('alone', 'foo', {
exact: false,
})

expect(() =>
expect(queryByTestId('only-one-class')).not.toHaveClass('alone', {
exact: true,
}),
).toThrowError(/Expected the element not to have EXACTLY defined classes/)

expect(() =>
expect(queryByTestId('only-one-class')).toHaveClass('alone', 'foo', {
exact: true,
}),
).toThrowError(/Expected the element to have EXACTLY defined classes/)
})
34 changes: 33 additions & 1 deletion src/to-have-class.js
@@ -1,6 +1,20 @@
import {matcherHint, printExpected} from 'jest-matcher-utils'
import {checkHtmlElement, getMessage} from './utils'

function getExpectedClassNamesAndOptions(params) {
const lastParam = params.pop()
let expectedClassNames, options

if (typeof lastParam === 'object') {
expectedClassNames = params
options = lastParam
} else {
expectedClassNames = params.concat(lastParam)
options = { exact: false }
}
return {expectedClassNames, options}
}

function splitClassNames(str) {
if (!str) {
return []
Expand All @@ -12,13 +26,31 @@ function isSubset(subset, superset) {
return subset.every(item => superset.includes(item))
}

export function toHaveClass(htmlElement, ...expectedClassNames) {
export function toHaveClass(htmlElement, ...params) {
checkHtmlElement(htmlElement, toHaveClass, this)
const {expectedClassNames, options} = getExpectedClassNamesAndOptions(params)

const received = splitClassNames(htmlElement.getAttribute('class'))
const expected = expectedClassNames.reduce(
(acc, className) => acc.concat(splitClassNames(className)),
[],
)

if (options.exact) {
return {
pass: isSubset(expected, received) && expected.length === received.length,
message: () => {
const to = this.isNot ? 'not to' : 'to'
return getMessage(
`Expected the element ${to} have EXACTLY defined classes`,
expected.join(' '),
'Received',
received.join(' '),
)
},
}
}

return expected.length > 0
? {
pass: isSubset(expected, received),
Expand Down

0 comments on commit cc8721e

Please sign in to comment.