Skip to content

Commit 9787ed5

Browse files
authoredJan 24, 2024
feat: Support for regular expressions in toHaveClass (#563)
1 parent 5675b86 commit 9787ed5

File tree

5 files changed

+90
-17
lines changed

5 files changed

+90
-17
lines changed
 

‎README.md

+8-3
Original file line numberDiff line numberDiff line change
@@ -753,10 +753,12 @@ toHaveClass(...classNames: string[], options?: {exact: boolean})
753753
```
754754

755755
This allows you to check whether the given element has certain classes within
756-
its `class` attribute.
756+
its `class` attribute. You must provide at least one class, unless you are
757+
asserting that an element does not have any classes.
757758

758-
You must provide at least one class, unless you are asserting that an element
759-
does not have any classes.
759+
The list of class names may include strings and regular expressions. Regular
760+
expressions are matched against each individual class in the target element, and
761+
it is NOT matched against its full `class` attribute value as whole.
760762

761763
#### Examples
762764

@@ -773,8 +775,11 @@ const noClasses = getByTestId('no-classes')
773775

774776
expect(deleteButton).toHaveClass('extra')
775777
expect(deleteButton).toHaveClass('btn-danger btn')
778+
expect(deleteButton).toHaveClass(/danger/, 'btn')
776779
expect(deleteButton).toHaveClass('btn-danger', 'btn')
777780
expect(deleteButton).not.toHaveClass('btn-link')
781+
expect(deleteButton).not.toHaveClass(/link/)
782+
expect(deleteButton).not.toHaveClass(/btn extra/) // It does not match
778783

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

‎src/__tests__/to-have-class.js

+57-6
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,32 @@ test('.toHaveClass', () => {
9393
).toThrowError(/(none)/)
9494
})
9595

96+
test('.toHaveClass with regular expressions', () => {
97+
const {queryByTestId} = renderElementWithClasses()
98+
99+
expect(queryByTestId('delete-button')).toHaveClass(/btn/)
100+
expect(queryByTestId('delete-button')).toHaveClass(/danger/)
101+
expect(queryByTestId('delete-button')).toHaveClass(
102+
/-danger$/,
103+
'extra',
104+
/^btn-[a-z]+$/,
105+
/\bbtn/,
106+
)
107+
108+
// It does not match with "btn extra", even though it is a substring of the
109+
// class "btn extra btn-danger". This is because the regular expression is
110+
// matched against each class individually.
111+
expect(queryByTestId('delete-button')).not.toHaveClass(/btn extra/)
112+
113+
expect(() =>
114+
expect(queryByTestId('delete-button')).not.toHaveClass(/danger/),
115+
).toThrowError()
116+
117+
expect(() =>
118+
expect(queryByTestId('delete-button')).toHaveClass(/dangerous/),
119+
).toThrowError()
120+
})
121+
96122
test('.toHaveClass with exact mode option', () => {
97123
const {queryByTestId} = renderElementWithClasses()
98124

@@ -102,19 +128,21 @@ test('.toHaveClass with exact mode option', () => {
102128
expect(queryByTestId('delete-button')).not.toHaveClass('btn extra', {
103129
exact: true,
104130
})
105-
expect(
106-
queryByTestId('delete-button'),
107-
).not.toHaveClass('btn extra btn-danger foo', {exact: true})
131+
expect(queryByTestId('delete-button')).not.toHaveClass(
132+
'btn extra btn-danger foo',
133+
{exact: true},
134+
)
108135

109136
expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', {
110137
exact: false,
111138
})
112139
expect(queryByTestId('delete-button')).toHaveClass('btn extra', {
113140
exact: false,
114141
})
115-
expect(
116-
queryByTestId('delete-button'),
117-
).not.toHaveClass('btn extra btn-danger foo', {exact: false})
142+
expect(queryByTestId('delete-button')).not.toHaveClass(
143+
'btn extra btn-danger foo',
144+
{exact: false},
145+
)
118146

119147
expect(queryByTestId('delete-button')).toHaveClass(
120148
'btn',
@@ -178,3 +206,26 @@ test('.toHaveClass with exact mode option', () => {
178206
}),
179207
).toThrowError(/Expected the element to have EXACTLY defined classes/)
180208
})
209+
210+
test('.toHaveClass combining {exact:true} and regular expressions throws an error', () => {
211+
const {queryByTestId} = renderElementWithClasses()
212+
213+
expect(() =>
214+
expect(queryByTestId('delete-button')).not.toHaveClass(/btn/, {
215+
exact: true,
216+
}),
217+
).toThrowError()
218+
219+
expect(() =>
220+
expect(queryByTestId('delete-button')).not.toHaveClass(
221+
/-danger$/,
222+
'extra',
223+
/\bbtn/,
224+
{exact: true},
225+
),
226+
).toThrowError()
227+
228+
expect(() =>
229+
expect(queryByTestId('delete-button')).toHaveClass(/danger/, {exact: true}),
230+
).toThrowError()
231+
})

‎src/to-have-class.js

+18-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function getExpectedClassNamesAndOptions(params) {
44
const lastParam = params.pop()
55
let expectedClassNames, options
66

7-
if (typeof lastParam === 'object') {
7+
if (typeof lastParam === 'object' && !(lastParam instanceof RegExp)) {
88
expectedClassNames = params
99
options = lastParam
1010
} else {
@@ -15,14 +15,16 @@ function getExpectedClassNamesAndOptions(params) {
1515
}
1616

1717
function splitClassNames(str) {
18-
if (!str) {
19-
return []
20-
}
18+
if (!str) return []
2119
return str.split(/\s+/).filter(s => s.length > 0)
2220
}
2321

2422
function isSubset(subset, superset) {
25-
return subset.every(item => superset.includes(item))
23+
return subset.every(strOrRegexp =>
24+
typeof strOrRegexp === 'string'
25+
? superset.includes(strOrRegexp)
26+
: superset.some(className => strOrRegexp.test(className)),
27+
)
2628
}
2729

2830
export function toHaveClass(htmlElement, ...params) {
@@ -31,10 +33,20 @@ export function toHaveClass(htmlElement, ...params) {
3133

3234
const received = splitClassNames(htmlElement.getAttribute('class'))
3335
const expected = expectedClassNames.reduce(
34-
(acc, className) => acc.concat(splitClassNames(className)),
36+
(acc, className) =>
37+
acc.concat(
38+
typeof className === 'string' || !className
39+
? splitClassNames(className)
40+
: className,
41+
),
3542
[],
3643
)
3744

45+
const hasRegExp = expected.some(className => className instanceof RegExp)
46+
if (options.exact && hasRegExp) {
47+
throw new Error('Exact option does not support RegExp expected class names')
48+
}
49+
3850
if (options.exact) {
3951
return {
4052
pass: isSubset(expected, received) && expected.length === received.length,

‎types/__tests__/jest/jest-custom-expect-types.test.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ customExpect(element).toHaveAttribute('attr', true)
3838
customExpect(element).toHaveAttribute('attr', 'yes')
3939
customExpect(element).toHaveClass()
4040
customExpect(element).toHaveClass('cls1')
41-
customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
41+
customExpect(element).toHaveClass(/cls/)
42+
customExpect(element).toHaveClass('cls1', 'cls2', /cls(3|4)/)
4243
customExpect(element).toHaveClass('cls1', {exact: true})
4344
customExpect(element).toHaveDisplayValue('str')
4445
customExpect(element).toHaveDisplayValue(['str1', 'str2'])
@@ -94,3 +95,6 @@ customExpect(element).toHaveErrorMessage(
9495

9596
// @ts-expect-error The types accidentally allowed any property by falling back to "any"
9697
customExpect(element).nonExistentProperty()
98+
99+
// @ts-expect-error
100+
customExpect(element).toHaveClass(/cls/, {exact: true})

‎types/matchers.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,14 @@ declare namespace matchers {
249249
* const noClasses = getByTestId('no-classes')
250250
* expect(deleteButton).toHaveClass('btn')
251251
* expect(deleteButton).toHaveClass('btn-danger xs')
252+
* expect(deleteButton).toHaveClass(/danger/, 'xs')
252253
* expect(deleteButton).toHaveClass('btn xs btn-danger', {exact: true})
253254
* expect(deleteButton).not.toHaveClass('btn xs btn-danger', {exact: true})
254255
* expect(noClasses).not.toHaveClass()
255256
* @see
256257
* [testing-library/jest-dom#tohaveclass](https://github.com/testing-library/jest-dom#tohaveclass)
257258
*/
258-
toHaveClass(...classNames: string[]): R
259+
toHaveClass(...classNames: Array<string | RegExp>): R
259260
toHaveClass(classNames: string, options?: {exact: boolean}): R
260261
/**
261262
* @description

0 commit comments

Comments
 (0)
Please sign in to comment.