Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
shuuji3 committed Apr 15, 2024
2 parents 00857c7 + bd82f64 commit 8009b89
Show file tree
Hide file tree
Showing 21 changed files with 882 additions and 127 deletions.
55 changes: 54 additions & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,62 @@
"code",
"ideas"
]
},
{
"login": "fpapado",
"name": "Fotis Papadogeorgopoulos",
"avatar_url": "https://avatars.githubusercontent.com/u/3210764?v=4",
"profile": "http://fotis.xyz",
"contributions": [
"code",
"doc",
"test"
]
},
{
"login": "jakeboone02",
"name": "Jake Boone",
"avatar_url": "https://avatars.githubusercontent.com/u/366438?v=4",
"profile": "https://github.com/jakeboone02",
"contributions": [
"code",
"test"
]
},
{
"login": "SteKoe",
"name": "Stephan Köninger",
"avatar_url": "https://avatars.githubusercontent.com/u/1809221?v=4",
"profile": "http://www.stekoe.de",
"contributions": [
"bug",
"code"
]
},
{
"login": "kryops",
"name": "Michael Manzinger",
"avatar_url": "https://avatars.githubusercontent.com/u/1042594?v=4",
"profile": "https://github.com/kryops",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "Dennis273",
"name": "Dennis Chen",
"avatar_url": "https://avatars.githubusercontent.com/u/19815164?v=4",
"profile": "https://github.com/Dennis273",
"contributions": [
"code"
]
}
],
"repoHost": "https://github.com",
"contributorsPerLine": 7,
"skipCi": false
"skipCi": false,
"commitType": "docs",
"commitConvention": "angular"
}
242 changes: 149 additions & 93 deletions README.md

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
},
"./matchers": {
"require": {
"types": "./types/matchers.d.ts",
"types": "./types/matchers-standalone.d.ts",
"default": "./dist/matchers.js"
},
"import": {
"types": "./types/matchers.d.ts",
"types": "./types/matchers-standalone.d.ts",
"default": "./dist/matchers.mjs"
}
},
Expand Down Expand Up @@ -60,7 +60,7 @@
"setup": "npm install && npm run validate -s",
"test": "kcd-scripts test",
"test:update": "npm test -- --updateSnapshot --coverage",
"test:types": "tsc -p types/__tests__/jest && tsc -p types/__tests__/jest-globals && tsc -p types/__tests__/vitest",
"test:types": "tsc -p types/__tests__/jest && tsc -p types/__tests__/jest-globals && tsc -p types/__tests__/vitest && tsc -p types/__tests__/bun",
"validate": "kcd-scripts validate && npm run test:types"
},
"files": [
Expand All @@ -86,12 +86,14 @@
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"lodash": "^4.17.15",
"lodash": "^4.17.21",
"redent": "^3.0.0"
},
"devDependencies": {
"@jest/globals": "^29.6.2",
"@rollup/plugin-commonjs": "^25.0.4",
"@types/bun": "latest",
"@types/web": "latest",
"expect": "^29.6.2",
"jest-environment-jsdom-sixteen": "^1.0.3",
"jest-watch-select-projects": "^2.0.0",
Expand All @@ -105,6 +107,7 @@
},
"peerDependencies": {
"@jest/globals": ">= 28",
"@types/bun": "latest",
"@types/jest": ">= 28",
"jest": ">= 28",
"vitest": ">= 0.32"
Expand All @@ -113,6 +116,9 @@
"@jest/globals": {
"optional": true
},
"@types/bun": {
"optional": true
},
"@types/jest": {
"optional": true
},
Expand Down
63 changes: 57 additions & 6 deletions src/__tests__/to-have-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,32 @@ test('.toHaveClass', () => {
).toThrowError(/(none)/)
})

test('.toHaveClass with regular expressions', () => {
const {queryByTestId} = renderElementWithClasses()

expect(queryByTestId('delete-button')).toHaveClass(/btn/)
expect(queryByTestId('delete-button')).toHaveClass(/danger/)
expect(queryByTestId('delete-button')).toHaveClass(
/-danger$/,
'extra',
/^btn-[a-z]+$/,
/\bbtn/,
)

// It does not match with "btn extra", even though it is a substring of the
// class "btn extra btn-danger". This is because the regular expression is
// matched against each class individually.
expect(queryByTestId('delete-button')).not.toHaveClass(/btn extra/)

expect(() =>
expect(queryByTestId('delete-button')).not.toHaveClass(/danger/),
).toThrowError()

expect(() =>
expect(queryByTestId('delete-button')).toHaveClass(/dangerous/),
).toThrowError()
})

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

Expand All @@ -102,19 +128,21 @@ test('.toHaveClass with exact mode option', () => {
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')).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')).not.toHaveClass(
'btn extra btn-danger foo',
{exact: false},
)

expect(queryByTestId('delete-button')).toHaveClass(
'btn',
Expand Down Expand Up @@ -178,3 +206,26 @@ test('.toHaveClass with exact mode option', () => {
}),
).toThrowError(/Expected the element to have EXACTLY defined classes/)
})

test('.toHaveClass combining {exact:true} and regular expressions throws an error', () => {
const {queryByTestId} = renderElementWithClasses()

expect(() =>
expect(queryByTestId('delete-button')).not.toHaveClass(/btn/, {
exact: true,
}),
).toThrowError()

expect(() =>
expect(queryByTestId('delete-button')).not.toHaveClass(
/-danger$/,
'extra',
/\bbtn/,
{exact: true},
),
).toThrowError()

expect(() =>
expect(queryByTestId('delete-button')).toHaveClass(/danger/, {exact: true}),
).toThrowError()
})
107 changes: 107 additions & 0 deletions src/__tests__/to-have-role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {render} from './helpers/test-utils'

describe('.toHaveRole', () => {
it('matches implicit role', () => {
const {queryByTestId} = render(`
<div>
<button data-testid="continue-button">Continue</button>
</div>
`)

const continueButton = queryByTestId('continue-button')

expect(continueButton).not.toHaveRole('listitem')
expect(continueButton).toHaveRole('button')

expect(() => {
expect(continueButton).toHaveRole('listitem')
}).toThrow(/expected element to have role/i)
expect(() => {
expect(continueButton).not.toHaveRole('button')
}).toThrow(/expected element not to have role/i)
})

it('matches explicit role', () => {
const {queryByTestId} = render(`
<div>
<div role="button" data-testid="continue-button">Continue</div>
</div>
`)

const continueButton = queryByTestId('continue-button')

expect(continueButton).not.toHaveRole('listitem')
expect(continueButton).toHaveRole('button')

expect(() => {
expect(continueButton).toHaveRole('listitem')
}).toThrow(/expected element to have role/i)
expect(() => {
expect(continueButton).not.toHaveRole('button')
}).toThrow(/expected element not to have role/i)
})

it('matches multiple explicit roles', () => {
const {queryByTestId} = render(`
<div>
<div role="button switch" data-testid="continue-button">Continue</div>
</div>
`)

const continueButton = queryByTestId('continue-button')

expect(continueButton).not.toHaveRole('listitem')
expect(continueButton).toHaveRole('button')
expect(continueButton).toHaveRole('switch')

expect(() => {
expect(continueButton).toHaveRole('listitem')
}).toThrow(/expected element to have role/i)
expect(() => {
expect(continueButton).not.toHaveRole('button')
}).toThrow(/expected element not to have role/i)
expect(() => {
expect(continueButton).not.toHaveRole('switch')
}).toThrow(/expected element not to have role/i)
})

// At this point, we might be testing the details of getImplicitAriaRoles, but
// it's good to have a gut check
it('handles implicit roles with multiple conditions', () => {
const {queryByTestId} = render(`
<div>
<a href="/about" data-testid="link-valid">Actually a valid link</a>
<a data-testid="link-invalid">Not a valid link (missing href)</a>
</div>
`)

const validLink = queryByTestId('link-valid')
const invalidLink = queryByTestId('link-invalid')

// valid link has role 'link'
expect(validLink).not.toHaveRole('listitem')
expect(validLink).toHaveRole('link')

expect(() => {
expect(validLink).toHaveRole('listitem')
}).toThrow(/expected element to have role/i)
expect(() => {
expect(validLink).not.toHaveRole('link')
}).toThrow(/expected element not to have role/i)

// invalid link has role 'generic'
expect(invalidLink).not.toHaveRole('listitem')
expect(invalidLink).not.toHaveRole('link')
expect(invalidLink).toHaveRole('generic')

expect(() => {
expect(invalidLink).toHaveRole('listitem')
}).toThrow(/expected element to have role/i)
expect(() => {
expect(invalidLink).toHaveRole('link')
}).toThrow(/expected element to have role/i)
expect(() => {
expect(invalidLink).not.toHaveRole('generic')
}).toThrow(/expected element not to have role/i)
})
})
1 change: 1 addition & 0 deletions src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {toContainHTML} from './to-contain-html'
export {toHaveTextContent} from './to-have-text-content'
export {toHaveAccessibleDescription} from './to-have-accessible-description'
export {toHaveAccessibleErrorMessage} from './to-have-accessible-errormessage'
export {toHaveRole} from './to-have-role'
export {toHaveAccessibleName} from './to-have-accessible-name'
export {toHaveAttribute} from './to-have-attribute'
export {toHaveClass} from './to-have-class'
Expand Down
24 changes: 18 additions & 6 deletions src/to-have-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function getExpectedClassNamesAndOptions(params) {
const lastParam = params.pop()
let expectedClassNames, options

if (typeof lastParam === 'object') {
if (typeof lastParam === 'object' && !(lastParam instanceof RegExp)) {
expectedClassNames = params
options = lastParam
} else {
Expand All @@ -15,14 +15,16 @@ function getExpectedClassNamesAndOptions(params) {
}

function splitClassNames(str) {
if (!str) {
return []
}
if (!str) return []
return str.split(/\s+/).filter(s => s.length > 0)
}

function isSubset(subset, superset) {
return subset.every(item => superset.includes(item))
return subset.every(strOrRegexp =>
typeof strOrRegexp === 'string'
? superset.includes(strOrRegexp)
: superset.some(className => strOrRegexp.test(className)),
)
}

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

const received = splitClassNames(htmlElement.getAttribute('class'))
const expected = expectedClassNames.reduce(
(acc, className) => acc.concat(splitClassNames(className)),
(acc, className) =>
acc.concat(
typeof className === 'string' || !className
? splitClassNames(className)
: className,
),
[],
)

const hasRegExp = expected.some(className => className instanceof RegExp)
if (options.exact && hasRegExp) {
throw new Error('Exact option does not support RegExp expected class names')
}

if (options.exact) {
return {
pass: isSubset(expected, received) && expected.length === received.length,
Expand Down
7 changes: 3 additions & 4 deletions src/to-have-form-values.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import isEqualWith from 'lodash/isEqualWith.js'
import uniq from 'lodash/uniq.js'
import isEqualWith from 'lodash/isEqualWith'
import escape from 'css.escape'
import {
checkHtmlElement,
compareArraysAsSet,
getSingleElementValue,
compareArraysAsSet,
} from './utils'

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

0 comments on commit 8009b89

Please sign in to comment.