Skip to content

Commit 8009b89

Browse files
authored
Merge branch 'main' into patch-1
2 parents 00857c7 + bd82f64 commit 8009b89

21 files changed

+882
-127
lines changed

.all-contributorsrc

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,9 +697,62 @@
697697
"code",
698698
"ideas"
699699
]
700+
},
701+
{
702+
"login": "fpapado",
703+
"name": "Fotis Papadogeorgopoulos",
704+
"avatar_url": "https://avatars.githubusercontent.com/u/3210764?v=4",
705+
"profile": "http://fotis.xyz",
706+
"contributions": [
707+
"code",
708+
"doc",
709+
"test"
710+
]
711+
},
712+
{
713+
"login": "jakeboone02",
714+
"name": "Jake Boone",
715+
"avatar_url": "https://avatars.githubusercontent.com/u/366438?v=4",
716+
"profile": "https://github.com/jakeboone02",
717+
"contributions": [
718+
"code",
719+
"test"
720+
]
721+
},
722+
{
723+
"login": "SteKoe",
724+
"name": "Stephan Köninger",
725+
"avatar_url": "https://avatars.githubusercontent.com/u/1809221?v=4",
726+
"profile": "http://www.stekoe.de",
727+
"contributions": [
728+
"bug",
729+
"code"
730+
]
731+
},
732+
{
733+
"login": "kryops",
734+
"name": "Michael Manzinger",
735+
"avatar_url": "https://avatars.githubusercontent.com/u/1042594?v=4",
736+
"profile": "https://github.com/kryops",
737+
"contributions": [
738+
"bug",
739+
"code",
740+
"test"
741+
]
742+
},
743+
{
744+
"login": "Dennis273",
745+
"name": "Dennis Chen",
746+
"avatar_url": "https://avatars.githubusercontent.com/u/19815164?v=4",
747+
"profile": "https://github.com/Dennis273",
748+
"contributions": [
749+
"code"
750+
]
700751
}
701752
],
702753
"repoHost": "https://github.com",
703754
"contributorsPerLine": 7,
704-
"skipCi": false
755+
"skipCi": false,
756+
"commitType": "docs",
757+
"commitConvention": "angular"
705758
}

README.md

Lines changed: 149 additions & 93 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
},
2828
"./matchers": {
2929
"require": {
30-
"types": "./types/matchers.d.ts",
30+
"types": "./types/matchers-standalone.d.ts",
3131
"default": "./dist/matchers.js"
3232
},
3333
"import": {
34-
"types": "./types/matchers.d.ts",
34+
"types": "./types/matchers-standalone.d.ts",
3535
"default": "./dist/matchers.mjs"
3636
}
3737
},
@@ -60,7 +60,7 @@
6060
"setup": "npm install && npm run validate -s",
6161
"test": "kcd-scripts test",
6262
"test:update": "npm test -- --updateSnapshot --coverage",
63-
"test:types": "tsc -p types/__tests__/jest && tsc -p types/__tests__/jest-globals && tsc -p types/__tests__/vitest",
63+
"test:types": "tsc -p types/__tests__/jest && tsc -p types/__tests__/jest-globals && tsc -p types/__tests__/vitest && tsc -p types/__tests__/bun",
6464
"validate": "kcd-scripts validate && npm run test:types"
6565
},
6666
"files": [
@@ -86,12 +86,14 @@
8686
"chalk": "^3.0.0",
8787
"css.escape": "^1.5.1",
8888
"dom-accessibility-api": "^0.6.3",
89-
"lodash": "^4.17.15",
89+
"lodash": "^4.17.21",
9090
"redent": "^3.0.0"
9191
},
9292
"devDependencies": {
9393
"@jest/globals": "^29.6.2",
9494
"@rollup/plugin-commonjs": "^25.0.4",
95+
"@types/bun": "latest",
96+
"@types/web": "latest",
9597
"expect": "^29.6.2",
9698
"jest-environment-jsdom-sixteen": "^1.0.3",
9799
"jest-watch-select-projects": "^2.0.0",
@@ -105,6 +107,7 @@
105107
},
106108
"peerDependencies": {
107109
"@jest/globals": ">= 28",
110+
"@types/bun": "latest",
108111
"@types/jest": ">= 28",
109112
"jest": ">= 28",
110113
"vitest": ">= 0.32"
@@ -113,6 +116,9 @@
113116
"@jest/globals": {
114117
"optional": true
115118
},
119+
"@types/bun": {
120+
"optional": true
121+
},
116122
"@types/jest": {
117123
"optional": true
118124
},

src/__tests__/to-have-class.js

Lines changed: 57 additions & 6 deletions
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/__tests__/to-have-role.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {render} from './helpers/test-utils'
2+
3+
describe('.toHaveRole', () => {
4+
it('matches implicit role', () => {
5+
const {queryByTestId} = render(`
6+
<div>
7+
<button data-testid="continue-button">Continue</button>
8+
</div>
9+
`)
10+
11+
const continueButton = queryByTestId('continue-button')
12+
13+
expect(continueButton).not.toHaveRole('listitem')
14+
expect(continueButton).toHaveRole('button')
15+
16+
expect(() => {
17+
expect(continueButton).toHaveRole('listitem')
18+
}).toThrow(/expected element to have role/i)
19+
expect(() => {
20+
expect(continueButton).not.toHaveRole('button')
21+
}).toThrow(/expected element not to have role/i)
22+
})
23+
24+
it('matches explicit role', () => {
25+
const {queryByTestId} = render(`
26+
<div>
27+
<div role="button" data-testid="continue-button">Continue</div>
28+
</div>
29+
`)
30+
31+
const continueButton = queryByTestId('continue-button')
32+
33+
expect(continueButton).not.toHaveRole('listitem')
34+
expect(continueButton).toHaveRole('button')
35+
36+
expect(() => {
37+
expect(continueButton).toHaveRole('listitem')
38+
}).toThrow(/expected element to have role/i)
39+
expect(() => {
40+
expect(continueButton).not.toHaveRole('button')
41+
}).toThrow(/expected element not to have role/i)
42+
})
43+
44+
it('matches multiple explicit roles', () => {
45+
const {queryByTestId} = render(`
46+
<div>
47+
<div role="button switch" data-testid="continue-button">Continue</div>
48+
</div>
49+
`)
50+
51+
const continueButton = queryByTestId('continue-button')
52+
53+
expect(continueButton).not.toHaveRole('listitem')
54+
expect(continueButton).toHaveRole('button')
55+
expect(continueButton).toHaveRole('switch')
56+
57+
expect(() => {
58+
expect(continueButton).toHaveRole('listitem')
59+
}).toThrow(/expected element to have role/i)
60+
expect(() => {
61+
expect(continueButton).not.toHaveRole('button')
62+
}).toThrow(/expected element not to have role/i)
63+
expect(() => {
64+
expect(continueButton).not.toHaveRole('switch')
65+
}).toThrow(/expected element not to have role/i)
66+
})
67+
68+
// At this point, we might be testing the details of getImplicitAriaRoles, but
69+
// it's good to have a gut check
70+
it('handles implicit roles with multiple conditions', () => {
71+
const {queryByTestId} = render(`
72+
<div>
73+
<a href="/about" data-testid="link-valid">Actually a valid link</a>
74+
<a data-testid="link-invalid">Not a valid link (missing href)</a>
75+
</div>
76+
`)
77+
78+
const validLink = queryByTestId('link-valid')
79+
const invalidLink = queryByTestId('link-invalid')
80+
81+
// valid link has role 'link'
82+
expect(validLink).not.toHaveRole('listitem')
83+
expect(validLink).toHaveRole('link')
84+
85+
expect(() => {
86+
expect(validLink).toHaveRole('listitem')
87+
}).toThrow(/expected element to have role/i)
88+
expect(() => {
89+
expect(validLink).not.toHaveRole('link')
90+
}).toThrow(/expected element not to have role/i)
91+
92+
// invalid link has role 'generic'
93+
expect(invalidLink).not.toHaveRole('listitem')
94+
expect(invalidLink).not.toHaveRole('link')
95+
expect(invalidLink).toHaveRole('generic')
96+
97+
expect(() => {
98+
expect(invalidLink).toHaveRole('listitem')
99+
}).toThrow(/expected element to have role/i)
100+
expect(() => {
101+
expect(invalidLink).toHaveRole('link')
102+
}).toThrow(/expected element to have role/i)
103+
expect(() => {
104+
expect(invalidLink).not.toHaveRole('generic')
105+
}).toThrow(/expected element not to have role/i)
106+
})
107+
})

src/matchers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {toContainHTML} from './to-contain-html'
77
export {toHaveTextContent} from './to-have-text-content'
88
export {toHaveAccessibleDescription} from './to-have-accessible-description'
99
export {toHaveAccessibleErrorMessage} from './to-have-accessible-errormessage'
10+
export {toHaveRole} from './to-have-role'
1011
export {toHaveAccessibleName} from './to-have-accessible-name'
1112
export {toHaveAttribute} from './to-have-attribute'
1213
export {toHaveClass} from './to-have-class'

src/to-have-class.js

Lines changed: 18 additions & 6 deletions
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,

src/to-have-form-values.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import isEqualWith from 'lodash/isEqualWith.js'
2-
import uniq from 'lodash/uniq.js'
1+
import isEqualWith from 'lodash/isEqualWith'
32
import escape from 'css.escape'
43
import {
54
checkHtmlElement,
6-
compareArraysAsSet,
75
getSingleElementValue,
6+
compareArraysAsSet,
87
} from './utils'
98

109
// Returns the combined value of several elements that have the same name
1110
// e.g. radio buttons or groups of checkboxes
1211
function getMultiElementValue(elements) {
13-
const types = uniq(elements.map(element => element.type))
12+
const types = [...new Set(elements.map(element => element.type))]
1413
if (types.length !== 1) {
1514
throw new Error(
1615
'Multiple form elements with the same name must be of the same type',

0 commit comments

Comments
 (0)