Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add exact mode option for toHaveClass (#176) #217

Merged
merged 4 commits into from Mar 26, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 = {}
gnapse marked this conversation as resolved.
Show resolved Hide resolved
}
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