+ `)
+
+ const field = queryByTestId(input)
+ expect(field).toHaveAccessibleErrorMessage('Step 1 of 9000')
+ })
+ })
+
+ // These tests for the `.not` use cases will help us cover our bases and complete test coverage
+ describe('Negated Test Cases', () => {
+ it("Passes the test if an invalid `id` is provided for the target element's `aria-errormessage`", () => {
+ const secondId = 'id2'
+ const secondError = 'LISTEN TO ME!!!'
+
+ const {queryByTestId} = render(`
+
+ `)
+
+ const field = queryByTestId('input')
+ expect(field).not.toHaveAccessibleErrorMessage()
+ expect(field).not.toHaveAccessibleErrorMessage(error)
+ expect(field).not.toHaveAccessibleErrorMessage(new RegExp(error[0]))
+ expect(field).not.toHaveAccessibleErrorMessage(secondError)
+ expect(field).not.toHaveAccessibleErrorMessage(new RegExp(secondError[0]))
+ })
+
+ it('Passes the test if the target element is valid according to the WAI-ARIA spec', () => {
+ const {queryByTestId} = render(`
+
+ `)
+
+ const field = queryByTestId(input)
+ expect(field).not.toHaveAccessibleErrorMessage()
+ expect(field).not.toHaveAccessibleErrorMessage(error)
+ expect(field).not.toHaveAccessibleErrorMessage(new RegExp(error[0]))
+ })
+ })
+})
diff --git a/src/__tests__/to-have-class.js b/src/__tests__/to-have-class.js
index 85bf8538..abfcf5d9 100644
--- a/src/__tests__/to-have-class.js
+++ b/src/__tests__/to-have-class.js
@@ -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()
@@ -102,9 +128,10 @@ 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,
@@ -112,9 +139,10 @@ test('.toHaveClass with exact mode option', () => {
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',
@@ -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()
+})
diff --git a/src/__tests__/to-have-role.js b/src/__tests__/to-have-role.js
new file mode 100644
index 00000000..c9e039ca
--- /dev/null
+++ b/src/__tests__/to-have-role.js
@@ -0,0 +1,107 @@
+import {render} from './helpers/test-utils'
+
+describe('.toHaveRole', () => {
+ it('matches implicit role', () => {
+ const {queryByTestId} = render(`
+
+
+
+ `)
+
+ 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(`
+
+
Continue
+
+ `)
+
+ 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(`
+
+
Continue
+
+ `)
+
+ 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(`
+
`)
+ const element = container.firstChild
+ const range = element.ownerDocument.createRange()
+ range.selectNodeContents(element)
+ element.ownerDocument.getSelection().addRange(range)
+
+ expect(() =>
+ expect(element).toHaveSelection(1),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `expected selection must be a string or undefined`,
+ )
+ })
+})
diff --git a/src/__tests__/to-have-style.js b/src/__tests__/to-have-style.js
index 0d94efaa..5991a7e9 100644
--- a/src/__tests__/to-have-style.js
+++ b/src/__tests__/to-have-style.js
@@ -70,6 +70,7 @@ describe('.toHaveStyle', () => {
background-color: black;
color: white;
float: left;
+ --var-name: 0px;
transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(1.175, 0.885, 0.32, 1.275);
}
`
@@ -92,6 +93,11 @@ describe('.toHaveStyle', () => {
),
).toThrowError()
+ // Custom property names are case sensitive
+ expect(() =>
+ expect(container.querySelector('.label')).toHaveStyle('--VAR-NAME: 0px;'),
+ ).toThrowError()
+
// Make sure the test fails if the css syntax is not valid
expect(() =>
expect(container.querySelector('.label')).not.toHaveStyle(
@@ -119,11 +125,11 @@ describe('.toHaveStyle', () => {
)
})
- test('handles inline custom properties', () => {
+ test('handles inline custom properties (with uppercase letters)', () => {
const {queryByTestId} = render(`
- Hello World
+ Hello World
`)
- expect(queryByTestId('color-example')).toHaveStyle('--color: blue')
+ expect(queryByTestId('color-example')).toHaveStyle('--accentColor: blue')
})
test('handles global custom properties', () => {
@@ -205,7 +211,7 @@ describe('.toHaveStyle', () => {
Hello World
`)
expect(queryByTestId('color-example')).toHaveStyle({
- fontSize: 12
+ fontSize: 12,
})
})
@@ -214,7 +220,7 @@ describe('.toHaveStyle', () => {
Hello World
`)
expect(() => {
- expect(queryByTestId('color-example')).toHaveStyle({ fontSize: '12px' })
+ expect(queryByTestId('color-example')).toHaveStyle({fontSize: '12px'})
}).toThrowError()
})
diff --git a/src/__tests__/to-have-value.js b/src/__tests__/to-have-value.js
index 80cc7828..1b276fb5 100644
--- a/src/__tests__/to-have-value.js
+++ b/src/__tests__/to-have-value.js
@@ -203,4 +203,18 @@ Received:
foo>
`)
})
+
+ test('handles value of aria-valuenow', () => {
+ const valueToCheck = 70
+ const {queryByTestId} = render(`
+
+
+ `)
+
+ expect(queryByTestId('meter')).toHaveValue(valueToCheck)
+ expect(queryByTestId('meter')).not.toHaveValue(valueToCheck + 1)
+
+ // Role that does not support aria-valuenow
+ expect(queryByTestId('textbox')).not.toHaveValue(70)
+ })
})
diff --git a/src/index.js b/src/index.js
index 8cecbe35..3801a1d5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1 +1,3 @@
-import './extend-expect'
+import * as extensions from './matchers'
+
+expect.extend(extensions)
diff --git a/src/jest-globals.js b/src/jest-globals.js
new file mode 100644
index 00000000..ac36d474
--- /dev/null
+++ b/src/jest-globals.js
@@ -0,0 +1,6 @@
+/* istanbul ignore file */
+
+import {expect} from '@jest/globals'
+import * as extensions from './matchers'
+
+expect.extend(extensions)
diff --git a/src/matchers.js b/src/matchers.js
index c90945d5..ed534e28 100644
--- a/src/matchers.js
+++ b/src/matchers.js
@@ -1,53 +1,27 @@
-import {toBeInTheDOM} from './to-be-in-the-dom'
-import {toBeInTheDocument} from './to-be-in-the-document'
-import {toBeEmpty} from './to-be-empty'
-import {toBeEmptyDOMElement} from './to-be-empty-dom-element'
-import {toContainElement} from './to-contain-element'
-import {toContainHTML} from './to-contain-html'
-import {toHaveTextContent} from './to-have-text-content'
-import {toHaveAccessibleDescription} from './to-have-accessible-description'
-import {toHaveAccessibleName} from './to-have-accessible-name'
-import {toHaveAttribute} from './to-have-attribute'
-import {toHaveClass} from './to-have-class'
-import {toHaveStyle} from './to-have-style'
-import {toHaveFocus} from './to-have-focus'
-import {toHaveFormValues} from './to-have-form-values'
-import {toBeVisible} from './to-be-visible'
-import {toBeDisabled, toBeEnabled} from './to-be-disabled'
-import {toBeRequired} from './to-be-required'
-import {toBeInvalid, toBeValid} from './to-be-invalid'
-import {toHaveValue} from './to-have-value'
-import {toHaveDisplayValue} from './to-have-display-value'
-import {toBeChecked} from './to-be-checked'
-import {toBePartiallyChecked} from './to-be-partially-checked'
-import {toHaveDescription} from './to-have-description'
-import {toHaveErrorMessage} from './to-have-errormessage'
-
-export {
- toBeInTheDOM,
- toBeInTheDocument,
- toBeEmpty,
- toBeEmptyDOMElement,
- toContainElement,
- toContainHTML,
- toHaveTextContent,
- toHaveAccessibleDescription,
- toHaveAccessibleName,
- toHaveAttribute,
- toHaveClass,
- toHaveStyle,
- toHaveFocus,
- toHaveFormValues,
- toBeVisible,
- toBeDisabled,
- toBeEnabled,
- toBeRequired,
- toBeInvalid,
- toBeValid,
- toHaveValue,
- toHaveDisplayValue,
- toBeChecked,
- toBePartiallyChecked,
- toHaveDescription,
- toHaveErrorMessage,
-}
+export {toBeInTheDOM} from './to-be-in-the-dom'
+export {toBeInTheDocument} from './to-be-in-the-document'
+export {toBeEmpty} from './to-be-empty'
+export {toBeEmptyDOMElement} from './to-be-empty-dom-element'
+export {toContainElement} from './to-contain-element'
+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'
+export {toHaveStyle} from './to-have-style'
+export {toHaveFocus} from './to-have-focus'
+export {toHaveFormValues} from './to-have-form-values'
+export {toBeVisible} from './to-be-visible'
+export {toBeDisabled, toBeEnabled} from './to-be-disabled'
+export {toBeRequired} from './to-be-required'
+export {toBeInvalid, toBeValid} from './to-be-invalid'
+export {toHaveValue} from './to-have-value'
+export {toHaveDisplayValue} from './to-have-display-value'
+export {toBeChecked} from './to-be-checked'
+export {toBePartiallyChecked} from './to-be-partially-checked'
+export {toHaveDescription} from './to-have-description'
+export {toHaveErrorMessage} from './to-have-errormessage'
+export {toHaveSelection} from './to-have-selection'
diff --git a/src/to-be-in-the-document.js b/src/to-be-in-the-document.js
index 8ccc451a..a7eda78e 100644
--- a/src/to-be-in-the-document.js
+++ b/src/to-be-in-the-document.js
@@ -29,7 +29,7 @@ export function toBeInTheDocument(element) {
'',
),
'',
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
this.utils.RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()),
].join('\n')
},
diff --git a/src/to-be-required.js b/src/to-be-required.js
index d674a62c..39188e63 100644
--- a/src/to-be-required.js
+++ b/src/to-be-required.js
@@ -15,10 +15,13 @@ const UNSUPPORTED_INPUT_TYPES = [
]
const SUPPORTED_ARIA_ROLES = [
+ 'checkbox',
'combobox',
'gridcell',
+ 'listbox',
'radiogroup',
'spinbutton',
+ 'textbox',
'tree',
]
diff --git a/src/to-contain-element.js b/src/to-contain-element.js
index c94ddbf9..445a6120 100644
--- a/src/to-contain-element.js
+++ b/src/to-contain-element.js
@@ -17,7 +17,7 @@ export function toContainElement(container, element) {
'element',
),
'',
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
this.utils.RECEIVED_COLOR(`${this.utils.stringify(
container.cloneNode(false),
)} ${
diff --git a/src/to-contain-html.js b/src/to-contain-html.js
index ccbff5f5..30158ee1 100644
--- a/src/to-contain-html.js
+++ b/src/to-contain-html.js
@@ -23,7 +23,7 @@ export function toContainHTML(container, htmlText) {
'',
),
'Expected:',
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
` ${this.utils.EXPECTED_COLOR(htmlText)}`,
'Received:',
` ${this.utils.printReceived(container.cloneNode(true))}`,
diff --git a/src/to-have-accessible-errormessage.js b/src/to-have-accessible-errormessage.js
new file mode 100644
index 00000000..d34d310f
--- /dev/null
+++ b/src/to-have-accessible-errormessage.js
@@ -0,0 +1,85 @@
+import {checkHtmlElement, getMessage, normalize} from './utils'
+
+const ariaInvalidName = 'aria-invalid'
+const validStates = ['false']
+
+// See `aria-errormessage` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage
+export function toHaveAccessibleErrorMessage(
+ htmlElement,
+ expectedAccessibleErrorMessage,
+) {
+ checkHtmlElement(htmlElement, toHaveAccessibleErrorMessage, this)
+ const to = this.isNot ? 'not to' : 'to'
+ const method = this.isNot
+ ? '.not.toHaveAccessibleErrorMessage'
+ : '.toHaveAccessibleErrorMessage'
+
+ // Enforce Valid Id
+ const errormessageId = htmlElement.getAttribute('aria-errormessage')
+ const errormessageIdInvalid = !!errormessageId && /\s+/.test(errormessageId)
+
+ if (errormessageIdInvalid) {
+ return {
+ pass: false,
+ message: () => {
+ return getMessage(
+ this,
+ this.utils.matcherHint(method, 'element'),
+ "Expected element's `aria-errormessage` attribute to be empty or a single, valid ID",
+ '',
+ 'Received',
+ `aria-errormessage="${errormessageId}"`,
+ )
+ },
+ }
+ }
+
+ // See `aria-invalid` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-invalid
+ const ariaInvalidVal = htmlElement.getAttribute(ariaInvalidName)
+ const fieldValid =
+ !htmlElement.hasAttribute(ariaInvalidName) ||
+ validStates.includes(ariaInvalidVal)
+
+ // Enforce Valid `aria-invalid` Attribute
+ if (fieldValid) {
+ return {
+ pass: false,
+ message: () => {
+ return getMessage(
+ this,
+ this.utils.matcherHint(method, 'element'),
+ 'Expected element to be marked as invalid with attribute',
+ `${ariaInvalidName}="${String(true)}"`,
+ 'Received',
+ htmlElement.hasAttribute('aria-invalid')
+ ? `${ariaInvalidName}="${htmlElement.getAttribute(ariaInvalidName)}`
+ : null,
+ )
+ },
+ }
+ }
+
+ const error = normalize(
+ htmlElement.ownerDocument.getElementById(errormessageId)?.textContent ?? '',
+ )
+
+ return {
+ pass:
+ expectedAccessibleErrorMessage === undefined
+ ? Boolean(error)
+ : expectedAccessibleErrorMessage instanceof RegExp
+ ? expectedAccessibleErrorMessage.test(error)
+ : this.equals(error, expectedAccessibleErrorMessage),
+
+ message: () => {
+ return getMessage(
+ this,
+ this.utils.matcherHint(method, 'element'),
+ `Expected element ${to} have accessible error message`,
+ expectedAccessibleErrorMessage ?? '',
+ 'Received',
+ error,
+ )
+ },
+ }
+}
diff --git a/src/to-have-class.js b/src/to-have-class.js
index be59be3a..f59f4dd9 100644
--- a/src/to-have-class.js
+++ b/src/to-have-class.js
@@ -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 {
@@ -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) {
@@ -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,
diff --git a/src/to-have-errormessage.js b/src/to-have-errormessage.js
index a253b390..5b12e4e2 100644
--- a/src/to-have-errormessage.js
+++ b/src/to-have-errormessage.js
@@ -1,7 +1,8 @@
-import {checkHtmlElement, getMessage, normalize} from './utils'
+import {checkHtmlElement, getMessage, normalize, deprecate} from './utils'
// See aria-errormessage spec https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage
export function toHaveErrorMessage(htmlElement, checkWith) {
+ deprecate('toHaveErrorMessage', 'Please use toHaveAccessibleErrorMessage.')
checkHtmlElement(htmlElement, toHaveErrorMessage, this)
if (
diff --git a/src/to-have-form-values.js b/src/to-have-form-values.js
index c3fddcc0..cd479d68 100644
--- a/src/to-have-form-values.js
+++ b/src/to-have-form-values.js
@@ -1,16 +1,15 @@
-import isEqualWith from 'lodash/isEqualWith'
-import uniq from 'lodash/uniq'
+import isEqualWith from 'lodash/isEqualWith.js'
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',
diff --git a/src/to-have-role.js b/src/to-have-role.js
new file mode 100644
index 00000000..04b85ed8
--- /dev/null
+++ b/src/to-have-role.js
@@ -0,0 +1,147 @@
+import {elementRoles} from 'aria-query'
+import {checkHtmlElement, getMessage} from './utils'
+
+const elementRoleList = buildElementRoleList(elementRoles)
+
+export function toHaveRole(htmlElement, expectedRole) {
+ checkHtmlElement(htmlElement, toHaveRole, this)
+
+ const actualRoles = getExplicitOrImplicitRoles(htmlElement)
+ const pass = actualRoles.some(el => el === expectedRole)
+
+ return {
+ pass,
+
+ message: () => {
+ const to = this.isNot ? 'not to' : 'to'
+ return getMessage(
+ this,
+ this.utils.matcherHint(
+ `${this.isNot ? '.not' : ''}.${toHaveRole.name}`,
+ 'element',
+ '',
+ ),
+ `Expected element ${to} have role`,
+ expectedRole,
+ 'Received',
+ actualRoles.join(', '),
+ )
+ },
+ }
+}
+
+function getExplicitOrImplicitRoles(htmlElement) {
+ const hasExplicitRole = htmlElement.hasAttribute('role')
+
+ if (hasExplicitRole) {
+ const roleValue = htmlElement.getAttribute('role')
+
+ // Handle fallback roles, such as role="switch button"
+ // testing-library gates this behind the `queryFallbacks` flag; it is
+ // unclear why, but it makes sense to support this pattern out of the box
+ // https://testing-library.com/docs/queries/byrole/#queryfallbacks
+ return roleValue.split(' ').filter(Boolean)
+ }
+
+ const implicitRoles = getImplicitAriaRoles(htmlElement)
+
+ return implicitRoles
+}
+
+function getImplicitAriaRoles(currentNode) {
+ for (const {match, roles} of elementRoleList) {
+ if (match(currentNode)) {
+ return [...roles]
+ }
+ }
+
+ /* istanbul ignore next */
+ return [] // this does not get reached in practice, since elements have at least a 'generic' role
+}
+
+/**
+ * Transform the roles map (with required attributes and constraints) to a list
+ * of roles. Each item in the list has functions to match an element against it.
+ *
+ * Essentially copied over from [dom-testing-library's
+ * helpers](https://github.com/testing-library/dom-testing-library/blob/bd04cf95a1ed85a2238f7dfc1a77d5d16b4f59dc/src/role-helpers.js#L80)
+ *
+ * TODO: If we are truly just copying over stuff, would it make sense to move
+ * this to a separate package?
+ *
+ * TODO: This technique relies on CSS selectors; are those consistently
+ * available in all jest-dom environments? Why do other matchers in this package
+ * not use them like this?
+ */
+function buildElementRoleList(elementRolesMap) {
+ function makeElementSelector({name, attributes}) {
+ return `${name}${attributes
+ .map(({name: attributeName, value, constraints = []}) => {
+ const shouldNotExist = constraints.indexOf('undefined') !== -1
+ if (shouldNotExist) {
+ return `:not([${attributeName}])`
+ } else if (value) {
+ return `[${attributeName}="${value}"]`
+ } else {
+ return `[${attributeName}]`
+ }
+ })
+ .join('')}`
+ }
+
+ function getSelectorSpecificity({attributes = []}) {
+ return attributes.length
+ }
+
+ function bySelectorSpecificity(
+ {specificity: leftSpecificity},
+ {specificity: rightSpecificity},
+ ) {
+ return rightSpecificity - leftSpecificity
+ }
+
+ function match(element) {
+ let {attributes = []} = element
+
+ // https://github.com/testing-library/dom-testing-library/issues/814
+ const typeTextIndex = attributes.findIndex(
+ attribute =>
+ attribute.value &&
+ attribute.name === 'type' &&
+ attribute.value === 'text',
+ )
+
+ if (typeTextIndex >= 0) {
+ // not using splice to not mutate the attributes array
+ attributes = [
+ ...attributes.slice(0, typeTextIndex),
+ ...attributes.slice(typeTextIndex + 1),
+ ]
+ }
+
+ const selector = makeElementSelector({...element, attributes})
+
+ return node => {
+ if (typeTextIndex >= 0 && node.type !== 'text') {
+ return false
+ }
+
+ return node.matches(selector)
+ }
+ }
+
+ let result = []
+
+ for (const [element, roles] of elementRolesMap.entries()) {
+ result = [
+ ...result,
+ {
+ match: match(element),
+ roles: Array.from(roles),
+ specificity: getSelectorSpecificity(element),
+ },
+ ]
+ }
+
+ return result.sort(bySelectorSpecificity)
+}
diff --git a/src/to-have-selection.js b/src/to-have-selection.js
new file mode 100644
index 00000000..4eb74783
--- /dev/null
+++ b/src/to-have-selection.js
@@ -0,0 +1,114 @@
+import isEqualWith from 'lodash/isEqualWith.js'
+import {checkHtmlElement, compareArraysAsSet, getMessage} from './utils'
+
+/**
+ * Returns the selection from the element.
+ *
+ * @param element {HTMLElement} The element to get the selection from.
+ * @returns {String} The selection.
+ */
+function getSelection(element) {
+ const selection = element.ownerDocument.getSelection()
+
+ if (['input', 'textarea'].includes(element.tagName.toLowerCase())) {
+ if (['radio', 'checkbox'].includes(element.type)) return ''
+ return element.value
+ .toString()
+ .substring(element.selectionStart, element.selectionEnd)
+ }
+
+ if (selection.anchorNode === null || selection.focusNode === null) {
+ // No selection
+ return ''
+ }
+
+ const originalRange = selection.getRangeAt(0)
+ const temporaryRange = element.ownerDocument.createRange()
+
+ if (selection.containsNode(element, false)) {
+ // Whole element is inside selection
+ temporaryRange.selectNodeContents(element)
+ selection.removeAllRanges()
+ selection.addRange(temporaryRange)
+ } else if (
+ element.contains(selection.anchorNode) &&
+ element.contains(selection.focusNode)
+ ) {
+ // Element contains selection, nothing to do
+ } else {
+ // Element is partially selected
+ const selectionStartsWithinElement =
+ element === originalRange.startContainer ||
+ element.contains(originalRange.startContainer)
+ const selectionEndsWithinElement =
+ element === originalRange.endContainer ||
+ element.contains(originalRange.endContainer)
+ selection.removeAllRanges()
+
+ if (selectionStartsWithinElement || selectionEndsWithinElement) {
+ temporaryRange.selectNodeContents(element)
+
+ if (selectionStartsWithinElement) {
+ temporaryRange.setStart(
+ originalRange.startContainer,
+ originalRange.startOffset,
+ )
+ }
+ if (selectionEndsWithinElement) {
+ temporaryRange.setEnd(
+ originalRange.endContainer,
+ originalRange.endOffset,
+ )
+ }
+
+ selection.addRange(temporaryRange)
+ }
+ }
+
+ const result = selection.toString()
+
+ selection.removeAllRanges()
+ selection.addRange(originalRange)
+
+ return result
+}
+
+/**
+ * Checks if the element has the string selected.
+ *
+ * @param htmlElement {HTMLElement} The html element to check the selection for.
+ * @param expectedSelection {String} The selection as a string.
+ */
+export function toHaveSelection(htmlElement, expectedSelection) {
+ checkHtmlElement(htmlElement, toHaveSelection, this)
+
+ const expectsSelection = expectedSelection !== undefined
+
+ if (expectsSelection && typeof expectedSelection !== 'string') {
+ throw new Error(`expected selection must be a string or undefined`)
+ }
+
+ const receivedSelection = getSelection(htmlElement)
+
+ return {
+ pass: expectsSelection
+ ? isEqualWith(receivedSelection, expectedSelection, compareArraysAsSet)
+ : Boolean(receivedSelection),
+ message: () => {
+ const to = this.isNot ? 'not to' : 'to'
+ const matcher = this.utils.matcherHint(
+ `${this.isNot ? '.not' : ''}.toHaveSelection`,
+ 'element',
+ expectedSelection,
+ )
+ return getMessage(
+ this,
+ matcher,
+ `Expected the element ${to} have selection`,
+ expectsSelection ? expectedSelection : '(any)',
+ 'Received',
+ receivedSelection,
+ )
+ },
+ }
+}
diff --git a/src/to-have-style.js b/src/to-have-style.js
index f125450c..010e2a5d 100644
--- a/src/to-have-style.js
+++ b/src/to-have-style.js
@@ -17,11 +17,17 @@ function getStyleDeclaration(document, css) {
function isSubset(styles, computedStyle) {
return (
!!Object.keys(styles).length &&
- Object.entries(styles).every(
- ([prop, value]) =>
- computedStyle[prop] === value ||
- computedStyle.getPropertyValue(prop.toLowerCase()) === value,
- )
+ Object.entries(styles).every(([prop, value]) => {
+ const isCustomProperty = prop.startsWith('--')
+ const spellingVariants = [prop]
+ if (!isCustomProperty) spellingVariants.push(prop.toLowerCase())
+
+ return spellingVariants.some(
+ name =>
+ computedStyle[name] === value ||
+ computedStyle.getPropertyValue(name) === value,
+ )
+ })
)
}
diff --git a/src/to-have-value.js b/src/to-have-value.js
index 0b24e165..37cca52c 100644
--- a/src/to-have-value.js
+++ b/src/to-have-value.js
@@ -1,9 +1,9 @@
-import isEqualWith from 'lodash/isEqualWith'
+import isEqualWith from 'lodash/isEqualWith.js'
import {
checkHtmlElement,
- compareArraysAsSet,
getMessage,
getSingleElementValue,
+ compareArraysAsSet,
} from './utils'
export function toHaveValue(htmlElement, expectedValue) {
diff --git a/src/utils.js b/src/utils.js
index 2f07fb8e..2699d954 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,6 +1,5 @@
import redent from 'redent'
-import cssParse from 'css/lib/parse'
-import isEqual from 'lodash/isEqual'
+import {parse} from '@adobe/css-tools'
class GenericTypeError extends Error {
constructor(expectedString, received, matcherFn, context) {
@@ -28,7 +27,7 @@ class GenericTypeError extends Error {
'',
),
'',
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
`${context.utils.RECEIVED_COLOR(
'received',
)} value must ${expectedString}.`,
@@ -91,16 +90,16 @@ class InvalidCSSError extends Error {
this.message = [
received.message,
'',
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
context.utils.RECEIVED_COLOR(`Failing css:`),
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
context.utils.RECEIVED_COLOR(`${received.css}`),
].join('\n')
}
}
function parseCSS(css, ...args) {
- const ast = cssParse(`selector { ${css} }`, {silent: true}).stylesheet
+ const ast = parse(`selector { ${css} }`, {silent: true}).stylesheet
if (ast.parsingErrors && ast.parsingErrors.length > 0) {
const {reason, line} = ast.parsingErrors[0]
@@ -137,11 +136,11 @@ function getMessage(
) {
return [
`${matcher}\n`,
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
`${expectedLabel}:\n${context.utils.EXPECTED_COLOR(
redent(display(context, expectedValue), 2),
)}`,
- // eslint-disable-next-line @babel/new-cap
+ // eslint-disable-next-line new-cap
`${receivedLabel}:\n${context.utils.RECEIVED_COLOR(
redent(display(context, receivedValue), 2),
)}`,
@@ -197,26 +196,29 @@ function getInputValue(inputElement) {
}
}
+const rolesSupportingValues = ['meter', 'progressbar', 'slider', 'spinbutton']
+function getAccessibleValue(element) {
+ if (!rolesSupportingValues.includes(element.getAttribute('role'))) {
+ return undefined
+ }
+ return Number(element.getAttribute('aria-valuenow'))
+}
+
function getSingleElementValue(element) {
/* istanbul ignore if */
if (!element) {
return undefined
}
+
switch (element.tagName.toLowerCase()) {
case 'input':
return getInputValue(element)
case 'select':
return getSelectValue(element)
- default:
- return element.value
- }
-}
-
-function compareArraysAsSet(a, b) {
- if (Array.isArray(a) && Array.isArray(b)) {
- return isEqual(new Set(a), new Set(b))
+ default: {
+ return element.value ?? getAccessibleValue(element)
+ }
}
- return undefined
}
function toSentence(
@@ -228,6 +230,13 @@ function toSentence(
)
}
+function compareArraysAsSet(arr1, arr2) {
+ if (Array.isArray(arr1) && Array.isArray(arr2)) {
+ return [...new Set(arr1)].every(v => new Set(arr2).has(v))
+ }
+ return undefined
+}
+
export {
HtmlElementTypeError,
NodeTypeError,
@@ -240,6 +249,6 @@ export {
normalize,
getTag,
getSingleElementValue,
- compareArraysAsSet,
toSentence,
+ compareArraysAsSet,
}
diff --git a/src/extend-expect.js b/src/vitest.js
similarity index 53%
rename from src/extend-expect.js
rename to src/vitest.js
index 3801a1d5..a6b56cef 100644
--- a/src/extend-expect.js
+++ b/src/vitest.js
@@ -1,3 +1,6 @@
+/* istanbul ignore file */
+
+import {expect} from 'vitest'
import * as extensions from './matchers'
expect.extend(extensions)
diff --git a/tests/setup-env.js b/tests/setup-env.js
index a9325d25..151f6e7b 100644
--- a/tests/setup-env.js
+++ b/tests/setup-env.js
@@ -1,4 +1,4 @@
import {plugins} from 'pretty-format'
-import '../src/extend-expect'
+import '../src/index'
expect.addSnapshotSerializer(plugins.ConvertAnsi)
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..dec023f5
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true
+ },
+ "include": ["*.d.ts", "types"],
+ "exclude": ["types/__tests__"]
+}
diff --git a/types/__tests__/bun/bun-custom-expect-types.test.ts b/types/__tests__/bun/bun-custom-expect-types.test.ts
new file mode 100644
index 00000000..1467e913
--- /dev/null
+++ b/types/__tests__/bun/bun-custom-expect-types.test.ts
@@ -0,0 +1,100 @@
+/**
+ * File that tests whether the TypeScript typings work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import {expect} from 'bun:test'
+import * as matchersStandalone from '../../matchers-standalone'
+import * as originalMatchers from '../../matchers'
+
+expect.extend(matchersStandalone)
+
+const element: HTMLElement = document.body
+
+function customExpect(
+ _actual: HTMLElement,
+):
+ | originalMatchers.TestingLibraryMatchers
+ | originalMatchers.TestingLibraryMatchers> {
+ throw new Error('Method not implemented.')
+}
+
+customExpect(element).toBeInTheDOM()
+customExpect(element).toBeInTheDOM(document.body)
+customExpect(element).toBeInTheDocument()
+customExpect(element).toBeVisible()
+customExpect(element).toBeEmpty()
+customExpect(element).toBeDisabled()
+customExpect(element).toBeEnabled()
+customExpect(element).toBeInvalid()
+customExpect(element).toBeRequired()
+customExpect(element).toBeValid()
+customExpect(element).toContainElement(document.body)
+customExpect(element).toContainElement(null)
+customExpect(element).toContainHTML('body')
+customExpect(element).toHaveAttribute('attr')
+customExpect(element).toHaveAttribute('attr', true)
+customExpect(element).toHaveAttribute('attr', 'yes')
+customExpect(element).toHaveClass()
+customExpect(element).toHaveClass('cls1')
+customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+customExpect(element).toHaveClass('cls1', {exact: true})
+customExpect(element).toHaveDisplayValue('str')
+customExpect(element).toHaveDisplayValue(['str1', 'str2'])
+customExpect(element).toHaveDisplayValue(/str/)
+customExpect(element).toHaveDisplayValue([/str1/, 'str2'])
+customExpect(element).toHaveFocus()
+customExpect(element).toHaveFormValues({foo: 'bar', baz: 1})
+customExpect(element).toHaveStyle('display: block')
+customExpect(element).toHaveStyle({display: 'block', width: 100})
+customExpect(element).toHaveTextContent('Text')
+customExpect(element).toHaveTextContent(/Text/)
+customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+customExpect(element).toHaveValue()
+customExpect(element).toHaveValue('str')
+customExpect(element).toHaveValue(['str1', 'str2'])
+customExpect(element).toHaveValue(1)
+customExpect(element).toHaveValue(null)
+customExpect(element).toBeChecked()
+customExpect(element).toHaveDescription('some description')
+customExpect(element).toHaveDescription(/some description/)
+customExpect(element).toHaveDescription(expect.stringContaining('partial'))
+customExpect(element).toHaveDescription()
+customExpect(element).toHaveAccessibleDescription('some description')
+customExpect(element).toHaveAccessibleDescription(/some description/)
+customExpect(element).toHaveAccessibleDescription(
+ expect.stringContaining('partial'),
+)
+customExpect(element).toHaveAccessibleDescription()
+
+customExpect(element).toHaveAccessibleErrorMessage()
+customExpect(element).toHaveAccessibleErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i)
+customExpect(element).toHaveAccessibleErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+customExpect(element).toHaveAccessibleName('a label')
+customExpect(element).toHaveAccessibleName(/a label/)
+customExpect(element).toHaveAccessibleName(
+ expect.stringContaining('partial label'),
+)
+customExpect(element).toHaveAccessibleName()
+customExpect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveErrorMessage(/invalid time/i)
+customExpect(element).toHaveErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+customExpect(element).toHaveRole('button')
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+customExpect(element).nonExistentProperty()
diff --git a/types/__tests__/bun/bun-types.test.ts b/types/__tests__/bun/bun-types.test.ts
new file mode 100644
index 00000000..432dd867
--- /dev/null
+++ b/types/__tests__/bun/bun-types.test.ts
@@ -0,0 +1,120 @@
+/**
+ * File that tests whether the TypeScript typings for @types/jest work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import {expect} from 'bun:test'
+import '../../bun'
+
+const element: HTMLElement = document.body
+
+expect(element).toBeInTheDOM()
+expect(element).toBeInTheDOM(document.body)
+expect(element).toBeInTheDocument()
+expect(element).toBeVisible()
+expect(element).toBeEmpty()
+expect(element).toBeDisabled()
+expect(element).toBeEnabled()
+expect(element).toBeInvalid()
+expect(element).toBeRequired()
+expect(element).toBeValid()
+expect(element).toContainElement(document.body)
+expect(element).toContainElement(null)
+expect(element).toContainHTML('body')
+expect(element).toHaveAttribute('attr')
+expect(element).toHaveAttribute('attr', true)
+expect(element).toHaveAttribute('attr', 'yes')
+expect(element).toHaveClass()
+expect(element).toHaveClass('cls1')
+expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).toHaveClass('cls1', {exact: true})
+expect(element).toHaveDisplayValue('str')
+expect(element).toHaveDisplayValue(['str1', 'str2'])
+expect(element).toHaveDisplayValue(/str/)
+expect(element).toHaveDisplayValue([/str1/, 'str2'])
+expect(element).toHaveFocus()
+expect(element).toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).toHaveStyle('display: block')
+expect(element).toHaveStyle({display: 'block', width: 100})
+expect(element).toHaveTextContent('Text')
+expect(element).toHaveTextContent(/Text/)
+expect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).toHaveValue()
+expect(element).toHaveValue('str')
+expect(element).toHaveValue(['str1', 'str2'])
+expect(element).toHaveValue(1)
+expect(element).toHaveValue(null)
+expect(element).toBeChecked()
+expect(element).toHaveDescription('some description')
+expect(element).toHaveDescription(/some description/)
+expect(element).toHaveDescription(expect.stringContaining('partial'))
+expect(element).toHaveDescription()
+expect(element).toHaveAccessibleDescription('some description')
+expect(element).toHaveAccessibleDescription(/some description/)
+expect(element).toHaveAccessibleDescription(expect.stringContaining('partial'))
+expect(element).toHaveAccessibleDescription()
+expect(element).toHaveAccessibleName('a label')
+expect(element).toHaveAccessibleName(/a label/)
+expect(element).toHaveAccessibleName(expect.stringContaining('partial label'))
+expect(element).toHaveAccessibleName()
+expect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+expect(element).toHaveErrorMessage(/invalid time/i)
+expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time'))
+expect(element).toHaveRole('button')
+
+expect(element).not.toBeInTheDOM()
+expect(element).not.toBeInTheDOM(document.body)
+expect(element).not.toBeInTheDocument()
+expect(element).not.toBeVisible()
+expect(element).not.toBeEmpty()
+expect(element).not.toBeEmptyDOMElement()
+expect(element).not.toBeDisabled()
+expect(element).not.toBeEnabled()
+expect(element).not.toBeInvalid()
+expect(element).not.toBeRequired()
+expect(element).not.toBeValid()
+expect(element).not.toContainElement(document.body)
+expect(element).not.toContainElement(null)
+expect(element).not.toContainHTML('body')
+expect(element).not.toHaveAttribute('attr')
+expect(element).not.toHaveAttribute('attr', true)
+expect(element).not.toHaveAttribute('attr', 'yes')
+expect(element).not.toHaveClass()
+expect(element).not.toHaveClass('cls1')
+expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).not.toHaveClass('cls1', {exact: true})
+expect(element).not.toHaveDisplayValue('str')
+expect(element).not.toHaveDisplayValue(['str1', 'str2'])
+expect(element).not.toHaveDisplayValue(/str/)
+expect(element).not.toHaveDisplayValue([/str1/, 'str2'])
+expect(element).not.toHaveFocus()
+expect(element).not.toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).not.toHaveStyle('display: block')
+expect(element).not.toHaveTextContent('Text')
+expect(element).not.toHaveTextContent(/Text/)
+expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).not.toHaveValue()
+expect(element).not.toHaveValue('str')
+expect(element).not.toHaveValue(['str1', 'str2'])
+expect(element).not.toHaveValue(1)
+expect(element).not.toBeChecked()
+expect(element).not.toHaveDescription('some description')
+expect(element).not.toHaveDescription()
+expect(element).not.toHaveAccessibleDescription('some description')
+expect(element).not.toHaveAccessibleDescription()
+expect(element).not.toHaveAccessibleName('a label')
+expect(element).not.toHaveAccessibleName()
+expect(element).not.toBePartiallyChecked()
+expect(element).not.toHaveErrorMessage()
+expect(element).not.toHaveErrorMessage('Pikachu!')
+expect(element).not.toHaveRole('button')
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+expect(element).nonExistentProperty()
diff --git a/types/__tests__/bun/tsconfig.json b/types/__tests__/bun/tsconfig.json
new file mode 100644
index 00000000..2a7ced01
--- /dev/null
+++ b/types/__tests__/bun/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "types": ["bun", "web"]
+ },
+ "include": ["*.ts"]
+}
diff --git a/types/__tests__/jest-globals/jest-globals-custom-expect-types.test.ts b/types/__tests__/jest-globals/jest-globals-custom-expect-types.test.ts
new file mode 100644
index 00000000..96034dc1
--- /dev/null
+++ b/types/__tests__/jest-globals/jest-globals-custom-expect-types.test.ts
@@ -0,0 +1,97 @@
+/**
+ * File that tests whether the TypeScript typings work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import {expect} from '@jest/globals'
+import * as matchers from '../../matchers'
+
+expect.extend(matchers)
+
+const element: HTMLElement = document.body
+
+function customExpect(
+ _actual: HTMLElement,
+):
+ | matchers.TestingLibraryMatchers
+ | matchers.TestingLibraryMatchers> {
+ throw new Error('Method not implemented.')
+}
+
+customExpect(element).toBeInTheDOM()
+customExpect(element).toBeInTheDOM(document.body)
+customExpect(element).toBeInTheDocument()
+customExpect(element).toBeVisible()
+customExpect(element).toBeEmpty()
+customExpect(element).toBeDisabled()
+customExpect(element).toBeEnabled()
+customExpect(element).toBeInvalid()
+customExpect(element).toBeRequired()
+customExpect(element).toBeValid()
+customExpect(element).toContainElement(document.body)
+customExpect(element).toContainElement(null)
+customExpect(element).toContainHTML('body')
+customExpect(element).toHaveAttribute('attr')
+customExpect(element).toHaveAttribute('attr', true)
+customExpect(element).toHaveAttribute('attr', 'yes')
+customExpect(element).toHaveClass()
+customExpect(element).toHaveClass('cls1')
+customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+customExpect(element).toHaveClass('cls1', {exact: true})
+customExpect(element).toHaveDisplayValue('str')
+customExpect(element).toHaveDisplayValue(['str1', 'str2'])
+customExpect(element).toHaveDisplayValue(/str/)
+customExpect(element).toHaveDisplayValue([/str1/, 'str2'])
+customExpect(element).toHaveFocus()
+customExpect(element).toHaveFormValues({foo: 'bar', baz: 1})
+customExpect(element).toHaveStyle('display: block')
+customExpect(element).toHaveStyle({display: 'block', width: 100})
+customExpect(element).toHaveTextContent('Text')
+customExpect(element).toHaveTextContent(/Text/)
+customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+customExpect(element).toHaveValue()
+customExpect(element).toHaveValue('str')
+customExpect(element).toHaveValue(['str1', 'str2'])
+customExpect(element).toHaveValue(1)
+customExpect(element).toHaveValue(null)
+customExpect(element).toBeChecked()
+customExpect(element).toHaveDescription('some description')
+customExpect(element).toHaveDescription(/some description/)
+customExpect(element).toHaveDescription(expect.stringContaining('partial'))
+customExpect(element).toHaveDescription()
+customExpect(element).toHaveAccessibleDescription('some description')
+customExpect(element).toHaveAccessibleDescription(/some description/)
+customExpect(element).toHaveAccessibleDescription(
+ expect.stringContaining('partial'),
+)
+customExpect(element).toHaveAccessibleDescription()
+
+customExpect(element).toHaveAccessibleErrorMessage()
+customExpect(element).toHaveAccessibleErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i)
+customExpect(element).toHaveAccessibleErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+customExpect(element).toHaveAccessibleName('a label')
+customExpect(element).toHaveAccessibleName(/a label/)
+customExpect(element).toHaveAccessibleName(
+ expect.stringContaining('partial label'),
+)
+customExpect(element).toHaveAccessibleName()
+customExpect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveErrorMessage(/invalid time/i)
+customExpect(element).toHaveErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+customExpect(element).nonExistentProperty()
diff --git a/types/__tests__/jest-globals/jest-globals-types.test.ts b/types/__tests__/jest-globals/jest-globals-types.test.ts
new file mode 100644
index 00000000..150f8250
--- /dev/null
+++ b/types/__tests__/jest-globals/jest-globals-types.test.ts
@@ -0,0 +1,120 @@
+/**
+ * File that tests whether the TypeScript typings for @types/jest work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import {expect} from '@jest/globals'
+import '../../jest-globals'
+
+const element: HTMLElement = document.body
+
+expect(element).toBeInTheDOM()
+expect(element).toBeInTheDOM(document.body)
+expect(element).toBeInTheDocument()
+expect(element).toBeVisible()
+expect(element).toBeEmpty()
+expect(element).toBeDisabled()
+expect(element).toBeEnabled()
+expect(element).toBeInvalid()
+expect(element).toBeRequired()
+expect(element).toBeValid()
+expect(element).toContainElement(document.body)
+expect(element).toContainElement(null)
+expect(element).toContainHTML('body')
+expect(element).toHaveAttribute('attr')
+expect(element).toHaveAttribute('attr', true)
+expect(element).toHaveAttribute('attr', 'yes')
+expect(element).toHaveClass()
+expect(element).toHaveClass('cls1')
+expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).toHaveClass('cls1', {exact: true})
+expect(element).toHaveDisplayValue('str')
+expect(element).toHaveDisplayValue(['str1', 'str2'])
+expect(element).toHaveDisplayValue(/str/)
+expect(element).toHaveDisplayValue([/str1/, 'str2'])
+expect(element).toHaveFocus()
+expect(element).toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).toHaveStyle('display: block')
+expect(element).toHaveStyle({display: 'block', width: 100})
+expect(element).toHaveTextContent('Text')
+expect(element).toHaveTextContent(/Text/)
+expect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).toHaveValue()
+expect(element).toHaveValue('str')
+expect(element).toHaveValue(['str1', 'str2'])
+expect(element).toHaveValue(1)
+expect(element).toHaveValue(null)
+expect(element).toBeChecked()
+expect(element).toHaveDescription('some description')
+expect(element).toHaveDescription(/some description/)
+expect(element).toHaveDescription(expect.stringContaining('partial'))
+expect(element).toHaveDescription()
+expect(element).toHaveAccessibleDescription('some description')
+expect(element).toHaveAccessibleDescription(/some description/)
+expect(element).toHaveAccessibleDescription(expect.stringContaining('partial'))
+expect(element).toHaveAccessibleDescription()
+expect(element).toHaveAccessibleName('a label')
+expect(element).toHaveAccessibleName(/a label/)
+expect(element).toHaveAccessibleName(expect.stringContaining('partial label'))
+expect(element).toHaveAccessibleName()
+expect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+expect(element).toHaveErrorMessage(/invalid time/i)
+expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time'))
+expect(element).toHaveRole('button')
+
+expect(element).not.toBeInTheDOM()
+expect(element).not.toBeInTheDOM(document.body)
+expect(element).not.toBeInTheDocument()
+expect(element).not.toBeVisible()
+expect(element).not.toBeEmpty()
+expect(element).not.toBeEmptyDOMElement()
+expect(element).not.toBeDisabled()
+expect(element).not.toBeEnabled()
+expect(element).not.toBeInvalid()
+expect(element).not.toBeRequired()
+expect(element).not.toBeValid()
+expect(element).not.toContainElement(document.body)
+expect(element).not.toContainElement(null)
+expect(element).not.toContainHTML('body')
+expect(element).not.toHaveAttribute('attr')
+expect(element).not.toHaveAttribute('attr', true)
+expect(element).not.toHaveAttribute('attr', 'yes')
+expect(element).not.toHaveClass()
+expect(element).not.toHaveClass('cls1')
+expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).not.toHaveClass('cls1', {exact: true})
+expect(element).not.toHaveDisplayValue('str')
+expect(element).not.toHaveDisplayValue(['str1', 'str2'])
+expect(element).not.toHaveDisplayValue(/str/)
+expect(element).not.toHaveDisplayValue([/str1/, 'str2'])
+expect(element).not.toHaveFocus()
+expect(element).not.toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).not.toHaveStyle('display: block')
+expect(element).not.toHaveTextContent('Text')
+expect(element).not.toHaveTextContent(/Text/)
+expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).not.toHaveValue()
+expect(element).not.toHaveValue('str')
+expect(element).not.toHaveValue(['str1', 'str2'])
+expect(element).not.toHaveValue(1)
+expect(element).not.toBeChecked()
+expect(element).not.toHaveDescription('some description')
+expect(element).not.toHaveDescription()
+expect(element).not.toHaveAccessibleDescription('some description')
+expect(element).not.toHaveAccessibleDescription()
+expect(element).not.toHaveAccessibleName('a label')
+expect(element).not.toHaveAccessibleName()
+expect(element).not.toBePartiallyChecked()
+expect(element).not.toHaveErrorMessage()
+expect(element).not.toHaveErrorMessage('Pikachu!')
+expect(element).not.toHaveRole('button')
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+expect(element).nonExistentProperty()
diff --git a/types/__tests__/jest-globals/tsconfig.json b/types/__tests__/jest-globals/tsconfig.json
new file mode 100644
index 00000000..25a9cf27
--- /dev/null
+++ b/types/__tests__/jest-globals/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "types": []
+ },
+ "include": ["*.ts"]
+}
diff --git a/types/__tests__/jest/jest-custom-expect-types.test.ts b/types/__tests__/jest/jest-custom-expect-types.test.ts
new file mode 100644
index 00000000..6b64b8e5
--- /dev/null
+++ b/types/__tests__/jest/jest-custom-expect-types.test.ts
@@ -0,0 +1,100 @@
+/**
+ * File that tests whether the TypeScript typings work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import * as matchers from '../../matchers'
+
+expect.extend(matchers)
+
+const element: HTMLElement = document.body
+
+function customExpect(
+ _actual: HTMLElement,
+):
+ | matchers.TestingLibraryMatchers
+ | matchers.TestingLibraryMatchers> {
+ throw new Error('Method not implemented.')
+}
+
+customExpect(element).toBeInTheDOM()
+customExpect(element).toBeInTheDOM(document.body)
+customExpect(element).toBeInTheDocument()
+customExpect(element).toBeVisible()
+customExpect(element).toBeEmpty()
+customExpect(element).toBeDisabled()
+customExpect(element).toBeEnabled()
+customExpect(element).toBeInvalid()
+customExpect(element).toBeRequired()
+customExpect(element).toBeValid()
+customExpect(element).toContainElement(document.body)
+customExpect(element).toContainElement(null)
+customExpect(element).toContainHTML('body')
+customExpect(element).toHaveAttribute('attr')
+customExpect(element).toHaveAttribute('attr', true)
+customExpect(element).toHaveAttribute('attr', 'yes')
+customExpect(element).toHaveClass()
+customExpect(element).toHaveClass('cls1')
+customExpect(element).toHaveClass(/cls/)
+customExpect(element).toHaveClass('cls1', 'cls2', /cls(3|4)/)
+customExpect(element).toHaveClass('cls1', {exact: true})
+customExpect(element).toHaveDisplayValue('str')
+customExpect(element).toHaveDisplayValue(['str1', 'str2'])
+customExpect(element).toHaveDisplayValue(/str/)
+customExpect(element).toHaveDisplayValue([/str1/, 'str2'])
+customExpect(element).toHaveFocus()
+customExpect(element).toHaveFormValues({foo: 'bar', baz: 1})
+customExpect(element).toHaveStyle('display: block')
+customExpect(element).toHaveStyle({display: 'block', width: 100})
+customExpect(element).toHaveTextContent('Text')
+customExpect(element).toHaveTextContent(/Text/)
+customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+customExpect(element).toHaveValue()
+customExpect(element).toHaveValue('str')
+customExpect(element).toHaveValue(['str1', 'str2'])
+customExpect(element).toHaveValue(1)
+customExpect(element).toHaveValue(null)
+customExpect(element).toBeChecked()
+customExpect(element).toHaveDescription('some description')
+customExpect(element).toHaveDescription(/some description/)
+customExpect(element).toHaveDescription(expect.stringContaining('partial'))
+customExpect(element).toHaveDescription()
+customExpect(element).toHaveAccessibleDescription('some description')
+customExpect(element).toHaveAccessibleDescription(/some description/)
+customExpect(element).toHaveAccessibleDescription(
+ expect.stringContaining('partial'),
+)
+customExpect(element).toHaveAccessibleDescription()
+
+customExpect(element).toHaveAccessibleErrorMessage()
+customExpect(element).toHaveAccessibleErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i)
+customExpect(element).toHaveAccessibleErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+customExpect(element).toHaveAccessibleName('a label')
+customExpect(element).toHaveAccessibleName(/a label/)
+customExpect(element).toHaveAccessibleName(
+ expect.stringContaining('partial label'),
+)
+customExpect(element).toHaveAccessibleName()
+customExpect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveErrorMessage(/invalid time/i)
+customExpect(element).toHaveErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+customExpect(element).nonExistentProperty()
+
+// @ts-expect-error
+customExpect(element).toHaveClass(/cls/, {exact: true})
diff --git a/types/__tests__/jest/jest-types.test.ts b/types/__tests__/jest/jest-types.test.ts
new file mode 100644
index 00000000..d9596c67
--- /dev/null
+++ b/types/__tests__/jest/jest-types.test.ts
@@ -0,0 +1,119 @@
+/**
+ * File that tests whether the TypeScript typings for @types/jest work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import '../../jest'
+
+const element: HTMLElement = document.body
+
+expect(element).toBeInTheDOM()
+expect(element).toBeInTheDOM(document.body)
+expect(element).toBeInTheDocument()
+expect(element).toBeVisible()
+expect(element).toBeEmpty()
+expect(element).toBeDisabled()
+expect(element).toBeEnabled()
+expect(element).toBeInvalid()
+expect(element).toBeRequired()
+expect(element).toBeValid()
+expect(element).toContainElement(document.body)
+expect(element).toContainElement(null)
+expect(element).toContainHTML('body')
+expect(element).toHaveAttribute('attr')
+expect(element).toHaveAttribute('attr', true)
+expect(element).toHaveAttribute('attr', 'yes')
+expect(element).toHaveClass()
+expect(element).toHaveClass('cls1')
+expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).toHaveClass('cls1', {exact: true})
+expect(element).toHaveDisplayValue('str')
+expect(element).toHaveDisplayValue(['str1', 'str2'])
+expect(element).toHaveDisplayValue(/str/)
+expect(element).toHaveDisplayValue([/str1/, 'str2'])
+expect(element).toHaveFocus()
+expect(element).toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).toHaveStyle('display: block')
+expect(element).toHaveStyle({display: 'block', width: 100})
+expect(element).toHaveTextContent('Text')
+expect(element).toHaveTextContent(/Text/)
+expect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).toHaveValue()
+expect(element).toHaveValue('str')
+expect(element).toHaveValue(['str1', 'str2'])
+expect(element).toHaveValue(1)
+expect(element).toHaveValue(null)
+expect(element).toBeChecked()
+expect(element).toHaveDescription('some description')
+expect(element).toHaveDescription(/some description/)
+expect(element).toHaveDescription(expect.stringContaining('partial'))
+expect(element).toHaveDescription()
+expect(element).toHaveAccessibleDescription('some description')
+expect(element).toHaveAccessibleDescription(/some description/)
+expect(element).toHaveAccessibleDescription(expect.stringContaining('partial'))
+expect(element).toHaveAccessibleDescription()
+expect(element).toHaveAccessibleName('a label')
+expect(element).toHaveAccessibleName(/a label/)
+expect(element).toHaveAccessibleName(expect.stringContaining('partial label'))
+expect(element).toHaveAccessibleName()
+expect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+expect(element).toHaveErrorMessage(/invalid time/i)
+expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time'))
+expect(element).toHaveRole('button')
+
+expect(element).not.toBeInTheDOM()
+expect(element).not.toBeInTheDOM(document.body)
+expect(element).not.toBeInTheDocument()
+expect(element).not.toBeVisible()
+expect(element).not.toBeEmpty()
+expect(element).not.toBeEmptyDOMElement()
+expect(element).not.toBeDisabled()
+expect(element).not.toBeEnabled()
+expect(element).not.toBeInvalid()
+expect(element).not.toBeRequired()
+expect(element).not.toBeValid()
+expect(element).not.toContainElement(document.body)
+expect(element).not.toContainElement(null)
+expect(element).not.toContainHTML('body')
+expect(element).not.toHaveAttribute('attr')
+expect(element).not.toHaveAttribute('attr', true)
+expect(element).not.toHaveAttribute('attr', 'yes')
+expect(element).not.toHaveClass()
+expect(element).not.toHaveClass('cls1')
+expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).not.toHaveClass('cls1', {exact: true})
+expect(element).not.toHaveDisplayValue('str')
+expect(element).not.toHaveDisplayValue(['str1', 'str2'])
+expect(element).not.toHaveDisplayValue(/str/)
+expect(element).not.toHaveDisplayValue([/str1/, 'str2'])
+expect(element).not.toHaveFocus()
+expect(element).not.toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).not.toHaveStyle('display: block')
+expect(element).not.toHaveTextContent('Text')
+expect(element).not.toHaveTextContent(/Text/)
+expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).not.toHaveValue()
+expect(element).not.toHaveValue('str')
+expect(element).not.toHaveValue(['str1', 'str2'])
+expect(element).not.toHaveValue(1)
+expect(element).not.toBeChecked()
+expect(element).not.toHaveDescription('some description')
+expect(element).not.toHaveDescription()
+expect(element).not.toHaveAccessibleDescription('some description')
+expect(element).not.toHaveAccessibleDescription()
+expect(element).not.toHaveAccessibleName('a label')
+expect(element).not.toHaveAccessibleName()
+expect(element).not.toBePartiallyChecked()
+expect(element).not.toHaveErrorMessage()
+expect(element).not.toHaveErrorMessage('Pikachu!')
+expect(element).not.toHaveRole('button')
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+expect(element).nonExistentProperty()
diff --git a/types/__tests__/jest/tsconfig.json b/types/__tests__/jest/tsconfig.json
new file mode 100644
index 00000000..52ecd5d4
--- /dev/null
+++ b/types/__tests__/jest/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "types": ["jest"]
+ },
+ "include": ["*.ts"]
+}
diff --git a/types/__tests__/vitest/tsconfig.json b/types/__tests__/vitest/tsconfig.json
new file mode 100644
index 00000000..25a9cf27
--- /dev/null
+++ b/types/__tests__/vitest/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "types": []
+ },
+ "include": ["*.ts"]
+}
diff --git a/types/__tests__/vitest/vitest-custom-expect-types.test.ts b/types/__tests__/vitest/vitest-custom-expect-types.test.ts
new file mode 100644
index 00000000..e9008966
--- /dev/null
+++ b/types/__tests__/vitest/vitest-custom-expect-types.test.ts
@@ -0,0 +1,97 @@
+/**
+ * File that tests whether the TypeScript typings work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import {expect} from 'vitest'
+import * as matchers from '../../matchers'
+
+expect.extend(matchers)
+
+const element: HTMLElement = document.body
+
+function customExpect(
+ _actual: HTMLElement,
+):
+ | matchers.TestingLibraryMatchers
+ | matchers.TestingLibraryMatchers> {
+ throw new Error('Method not implemented.')
+}
+
+customExpect(element).toBeInTheDOM()
+customExpect(element).toBeInTheDOM(document.body)
+customExpect(element).toBeInTheDocument()
+customExpect(element).toBeVisible()
+customExpect(element).toBeEmpty()
+customExpect(element).toBeDisabled()
+customExpect(element).toBeEnabled()
+customExpect(element).toBeInvalid()
+customExpect(element).toBeRequired()
+customExpect(element).toBeValid()
+customExpect(element).toContainElement(document.body)
+customExpect(element).toContainElement(null)
+customExpect(element).toContainHTML('body')
+customExpect(element).toHaveAttribute('attr')
+customExpect(element).toHaveAttribute('attr', true)
+customExpect(element).toHaveAttribute('attr', 'yes')
+customExpect(element).toHaveClass()
+customExpect(element).toHaveClass('cls1')
+customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+customExpect(element).toHaveClass('cls1', {exact: true})
+customExpect(element).toHaveDisplayValue('str')
+customExpect(element).toHaveDisplayValue(['str1', 'str2'])
+customExpect(element).toHaveDisplayValue(/str/)
+customExpect(element).toHaveDisplayValue([/str1/, 'str2'])
+customExpect(element).toHaveFocus()
+customExpect(element).toHaveFormValues({foo: 'bar', baz: 1})
+customExpect(element).toHaveStyle('display: block')
+customExpect(element).toHaveStyle({display: 'block', width: 100})
+customExpect(element).toHaveTextContent('Text')
+customExpect(element).toHaveTextContent(/Text/)
+customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+customExpect(element).toHaveValue()
+customExpect(element).toHaveValue('str')
+customExpect(element).toHaveValue(['str1', 'str2'])
+customExpect(element).toHaveValue(1)
+customExpect(element).toHaveValue(null)
+customExpect(element).toBeChecked()
+customExpect(element).toHaveDescription('some description')
+customExpect(element).toHaveDescription(/some description/)
+customExpect(element).toHaveDescription(expect.stringContaining('partial'))
+customExpect(element).toHaveDescription()
+customExpect(element).toHaveAccessibleDescription('some description')
+customExpect(element).toHaveAccessibleDescription(/some description/)
+customExpect(element).toHaveAccessibleDescription(
+ expect.stringContaining('partial'),
+)
+customExpect(element).toHaveAccessibleDescription()
+
+customExpect(element).toHaveAccessibleErrorMessage()
+customExpect(element).toHaveAccessibleErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i)
+customExpect(element).toHaveAccessibleErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+customExpect(element).toHaveAccessibleName('a label')
+customExpect(element).toHaveAccessibleName(/a label/)
+customExpect(element).toHaveAccessibleName(
+ expect.stringContaining('partial label'),
+)
+customExpect(element).toHaveAccessibleName()
+customExpect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+customExpect(element).toHaveErrorMessage(/invalid time/i)
+customExpect(element).toHaveErrorMessage(
+ expect.stringContaining('Invalid time'),
+)
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+customExpect(element).nonExistentProperty()
diff --git a/types/__tests__/vitest/vitest-types.test.ts b/types/__tests__/vitest/vitest-types.test.ts
new file mode 100644
index 00000000..7a90be05
--- /dev/null
+++ b/types/__tests__/vitest/vitest-types.test.ts
@@ -0,0 +1,120 @@
+/**
+ * File that tests whether the TypeScript typings for @types/jest work as expected.
+ */
+
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+
+import {expect} from 'vitest'
+import '../../vitest'
+
+const element: HTMLElement = document.body
+
+expect(element).toBeInTheDOM()
+expect(element).toBeInTheDOM(document.body)
+expect(element).toBeInTheDocument()
+expect(element).toBeVisible()
+expect(element).toBeEmpty()
+expect(element).toBeDisabled()
+expect(element).toBeEnabled()
+expect(element).toBeInvalid()
+expect(element).toBeRequired()
+expect(element).toBeValid()
+expect(element).toContainElement(document.body)
+expect(element).toContainElement(null)
+expect(element).toContainHTML('body')
+expect(element).toHaveAttribute('attr')
+expect(element).toHaveAttribute('attr', true)
+expect(element).toHaveAttribute('attr', 'yes')
+expect(element).toHaveClass()
+expect(element).toHaveClass('cls1')
+expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).toHaveClass('cls1', {exact: true})
+expect(element).toHaveDisplayValue('str')
+expect(element).toHaveDisplayValue(['str1', 'str2'])
+expect(element).toHaveDisplayValue(/str/)
+expect(element).toHaveDisplayValue([/str1/, 'str2'])
+expect(element).toHaveFocus()
+expect(element).toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).toHaveStyle('display: block')
+expect(element).toHaveStyle({display: 'block', width: 100})
+expect(element).toHaveTextContent('Text')
+expect(element).toHaveTextContent(/Text/)
+expect(element).toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).toHaveValue()
+expect(element).toHaveValue('str')
+expect(element).toHaveValue(['str1', 'str2'])
+expect(element).toHaveValue(1)
+expect(element).toHaveValue(null)
+expect(element).toBeChecked()
+expect(element).toHaveDescription('some description')
+expect(element).toHaveDescription(/some description/)
+expect(element).toHaveDescription(expect.stringContaining('partial'))
+expect(element).toHaveDescription()
+expect(element).toHaveAccessibleDescription('some description')
+expect(element).toHaveAccessibleDescription(/some description/)
+expect(element).toHaveAccessibleDescription(expect.stringContaining('partial'))
+expect(element).toHaveAccessibleDescription()
+expect(element).toHaveAccessibleName('a label')
+expect(element).toHaveAccessibleName(/a label/)
+expect(element).toHaveAccessibleName(expect.stringContaining('partial label'))
+expect(element).toHaveAccessibleName()
+expect(element).toHaveErrorMessage(
+ 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+)
+expect(element).toHaveErrorMessage(/invalid time/i)
+expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time'))
+expect(element).toHaveRole('button')
+
+expect(element).not.toBeInTheDOM()
+expect(element).not.toBeInTheDOM(document.body)
+expect(element).not.toBeInTheDocument()
+expect(element).not.toBeVisible()
+expect(element).not.toBeEmpty()
+expect(element).not.toBeEmptyDOMElement()
+expect(element).not.toBeDisabled()
+expect(element).not.toBeEnabled()
+expect(element).not.toBeInvalid()
+expect(element).not.toBeRequired()
+expect(element).not.toBeValid()
+expect(element).not.toContainElement(document.body)
+expect(element).not.toContainElement(null)
+expect(element).not.toContainHTML('body')
+expect(element).not.toHaveAttribute('attr')
+expect(element).not.toHaveAttribute('attr', true)
+expect(element).not.toHaveAttribute('attr', 'yes')
+expect(element).not.toHaveClass()
+expect(element).not.toHaveClass('cls1')
+expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4')
+expect(element).not.toHaveClass('cls1', {exact: true})
+expect(element).not.toHaveDisplayValue('str')
+expect(element).not.toHaveDisplayValue(['str1', 'str2'])
+expect(element).not.toHaveDisplayValue(/str/)
+expect(element).not.toHaveDisplayValue([/str1/, 'str2'])
+expect(element).not.toHaveFocus()
+expect(element).not.toHaveFormValues({foo: 'bar', baz: 1})
+expect(element).not.toHaveStyle('display: block')
+expect(element).not.toHaveTextContent('Text')
+expect(element).not.toHaveTextContent(/Text/)
+expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true})
+expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true})
+expect(element).not.toHaveValue()
+expect(element).not.toHaveValue('str')
+expect(element).not.toHaveValue(['str1', 'str2'])
+expect(element).not.toHaveValue(1)
+expect(element).not.toBeChecked()
+expect(element).not.toHaveDescription('some description')
+expect(element).not.toHaveDescription()
+expect(element).not.toHaveAccessibleDescription('some description')
+expect(element).not.toHaveAccessibleDescription()
+expect(element).not.toHaveAccessibleName('a label')
+expect(element).not.toHaveAccessibleName()
+expect(element).not.toBePartiallyChecked()
+expect(element).not.toHaveErrorMessage()
+expect(element).not.toHaveErrorMessage('Pikachu!')
+expect(element).not.toHaveRole('button')
+
+// @ts-expect-error The types accidentally allowed any property by falling back to "any"
+expect(element).nonExistentProperty()
diff --git a/types/bun.d.ts b/types/bun.d.ts
new file mode 100644
index 00000000..676a0ac3
--- /dev/null
+++ b/types/bun.d.ts
@@ -0,0 +1,11 @@
+import {type expect} from 'bun:test'
+import {type TestingLibraryMatchers} from './matchers'
+
+export {}
+declare module 'bun:test' {
+ interface Matchers
+ extends TestingLibraryMatchers<
+ ReturnType,
+ T
+ > {}
+}
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 00000000..cebdd1af
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1 @@
+///
diff --git a/types/jest-globals.d.ts b/types/jest-globals.d.ts
new file mode 100644
index 00000000..a6819079
--- /dev/null
+++ b/types/jest-globals.d.ts
@@ -0,0 +1,11 @@
+import {type expect} from '@jest/globals'
+import {type TestingLibraryMatchers} from './matchers'
+
+export {}
+declare module '@jest/expect' {
+ export interface Matchers>
+ extends TestingLibraryMatchers<
+ ReturnType,
+ R
+ > {}
+}
diff --git a/types/jest.d.ts b/types/jest.d.ts
new file mode 100644
index 00000000..1daed629
--- /dev/null
+++ b/types/jest.d.ts
@@ -0,0 +1,13 @@
+///
+
+import {type TestingLibraryMatchers} from './matchers'
+
+declare global {
+ namespace jest {
+ interface Matchers
+ extends TestingLibraryMatchers<
+ ReturnType,
+ R
+ > {}
+ }
+}
diff --git a/types/matchers-standalone.d.ts b/types/matchers-standalone.d.ts
new file mode 100644
index 00000000..175e9157
--- /dev/null
+++ b/types/matchers-standalone.d.ts
@@ -0,0 +1,30 @@
+import {type TestingLibraryMatchers as _TLM} from './matchers'
+
+interface MatcherReturnType {
+ pass: boolean
+ message: () => string
+}
+
+interface OverloadedMatchers {
+ toHaveClass(expected: any, ...rest: string[]) : MatcherReturnType
+ toHaveClass(
+ expected: any,
+ className: string,
+ options?: {exact: boolean},
+ ) : MatcherReturnType
+}
+
+declare namespace matchersStandalone {
+ type MatchersStandalone = {
+ [T in keyof _TLM]: (
+ expected: any,
+ ...rest: Parameters<_TLM[T]>
+ ) => MatcherReturnType
+ } & OverloadedMatchers
+
+ type TestingLibraryMatchers = _TLM
+}
+
+declare const matchersStandalone: matchersStandalone.MatchersStandalone &
+ Record
+export = matchersStandalone
diff --git a/types/matchers.d.ts b/types/matchers.d.ts
new file mode 100755
index 00000000..8bf2ad58
--- /dev/null
+++ b/types/matchers.d.ts
@@ -0,0 +1,771 @@
+import {type ARIARole} from 'aria-query'
+
+declare namespace matchers {
+ interface TestingLibraryMatchers {
+ /**
+ * @deprecated
+ * since v1.9.0
+ * @description
+ * Assert whether a value is a DOM element, or not. Contrary to what its name implies, this matcher only checks
+ * that you passed to it a valid DOM element.
+ *
+ * It does not have a clear definition of what "the DOM" is. Therefore, it does not check whether that element
+ * is contained anywhere.
+ * @see
+ * [testing-library/jest-dom#toBeInTheDom](https://github.com/testing-library/jest-dom#toBeInTheDom)
+ */
+ toBeInTheDOM(container?: HTMLElement | SVGElement): R
+ /**
+ * @description
+ * Assert whether an element is present in the document or not.
+ * @example
+ *
+ *
+ * expect(queryByTestId('svg-element')).toBeInTheDocument()
+ * expect(queryByTestId('does-not-exist')).not.toBeInTheDocument()
+ * @see
+ * [testing-library/jest-dom#tobeinthedocument](https://github.com/testing-library/jest-dom#tobeinthedocument)
+ */
+ toBeInTheDocument(): R
+ /**
+ * @description
+ * This allows you to check if an element is currently visible to the user.
+ *
+ * An element is visible if **all** the following conditions are met:
+ * * it does not have its css property display set to none
+ * * it does not have its css property visibility set to either hidden or collapse
+ * * it does not have its css property opacity set to 0
+ * * its parent element is also visible (and so on up to the top of the DOM tree)
+ * * it does not have the hidden attribute
+ * * if `` it has the open attribute
+ * @example
+ *
+ * Zero Opacity
+ *
+ *
+ *
Visible Example
+ *
+ * expect(getByTestId('zero-opacity')).not.toBeVisible()
+ * expect(getByTestId('visible')).toBeVisible()
+ * @see
+ * [testing-library/jest-dom#tobevisible](https://github.com/testing-library/jest-dom#tobevisible)
+ */
+ toBeVisible(): R
+ /**
+ * @deprecated
+ * since v5.9.0
+ * @description
+ * Assert whether an element has content or not.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('empty')).toBeEmpty()
+ * expect(getByTestId('not-empty')).not.toBeEmpty()
+ * @see
+ * [testing-library/jest-dom#tobeempty](https://github.com/testing-library/jest-dom#tobeempty)
+ */
+ toBeEmpty(): R
+ /**
+ * @description
+ * Assert whether an element has content or not.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('empty')).toBeEmptyDOMElement()
+ * expect(getByTestId('not-empty')).not.toBeEmptyDOMElement()
+ * @see
+ * [testing-library/jest-dom#tobeemptydomelement](https://github.com/testing-library/jest-dom#tobeemptydomelement)
+ */
+ toBeEmptyDOMElement(): R
+ /**
+ * @description
+ * Allows you to check whether an element is disabled from the user's perspective.
+ *
+ * Matches if the element is a form control and the `disabled` attribute is specified on this element or the
+ * element is a descendant of a form element with a `disabled` attribute.
+ * @example
+ *
+ *
+ * expect(getByTestId('button')).toBeDisabled()
+ * @see
+ * [testing-library/jest-dom#tobedisabled](https://github.com/testing-library/jest-dom#tobedisabled)
+ */
+ toBeDisabled(): R
+ /**
+ * @description
+ * Allows you to check whether an element is not disabled from the user's perspective.
+ *
+ * Works like `not.toBeDisabled()`.
+ *
+ * Use this matcher to avoid double negation in your tests.
+ * @example
+ *
+ *
+ * expect(getByTestId('button')).toBeEnabled()
+ * @see
+ * [testing-library/jest-dom#tobeenabled](https://github.com/testing-library/jest-dom#tobeenabled)
+ */
+ toBeEnabled(): R
+ /**
+ * @description
+ * Check if a form element, or the entire `form`, is currently invalid.
+ *
+ * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no
+ * value or a value of "true", or if the result of `checkValidity()` is false.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('no-aria-invalid')).not.toBeInvalid()
+ * expect(getByTestId('invalid-form')).toBeInvalid()
+ * @see
+ * [testing-library/jest-dom#tobeinvalid](https://github.com/testing-library/jest-dom#tobeinvalid)
+ */
+ toBeInvalid(): R
+ /**
+ * @description
+ * This allows you to check if a form element is currently required.
+ *
+ * An element is required if it is having a `required` or `aria-required="true"` attribute.
+ * @example
+ *
+ *
+ *
+ * expect(getByTestId('required-input')).toBeRequired()
+ * expect(getByTestId('supported-role')).not.toBeRequired()
+ * @see
+ * [testing-library/jest-dom#toberequired](https://github.com/testing-library/jest-dom#toberequired)
+ */
+ toBeRequired(): R
+ /**
+ * @description
+ * Allows you to check if a form element is currently required.
+ *
+ * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no
+ * value or a value of "false", or if the result of `checkValidity()` is true.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('no-aria-invalid')).not.toBeValid()
+ * expect(getByTestId('invalid-form')).toBeInvalid()
+ * @see
+ * [testing-library/jest-dom#tobevalid](https://github.com/testing-library/jest-dom#tobevalid)
+ */
+ toBeValid(): R
+ /**
+ * @description
+ * Allows you to assert whether an element contains another element as a descendant or not.
+ * @example
+ *
+ *
+ *
+ *
+ * const ancestor = getByTestId('ancestor')
+ * const descendant = getByTestId('descendant')
+ * const nonExistantElement = getByTestId('does-not-exist')
+ * expect(ancestor).toContainElement(descendant)
+ * expect(descendant).not.toContainElement(ancestor)
+ * expect(ancestor).not.toContainElement(nonExistantElement)
+ * @see
+ * [testing-library/jest-dom#tocontainelement](https://github.com/testing-library/jest-dom#tocontainelement)
+ */
+ toContainElement(element: HTMLElement | SVGElement | null): R
+ /**
+ * @description
+ * Assert whether a string representing a HTML element is contained in another element.
+ * @example
+ *
+ *
+ * expect(getByTestId('parent')).toContainHTML('')
+ * @see
+ * [testing-library/jest-dom#tocontainhtml](https://github.com/testing-library/jest-dom#tocontainhtml)
+ */
+ toContainHTML(htmlText: string): R
+ /**
+ * @description
+ * Allows you to check if a given element has an attribute or not.
+ *
+ * You can also optionally check that the attribute has a specific expected value or partial match using
+ * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) or
+ * [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp).
+ * @example
+ *
+ *
+ * expect(button).toHaveAttribute('disabled')
+ * expect(button).toHaveAttribute('type', 'submit')
+ * expect(button).not.toHaveAttribute('type', 'button')
+ * @see
+ * [testing-library/jest-dom#tohaveattribute](https://github.com/testing-library/jest-dom#tohaveattribute)
+ */
+ toHaveAttribute(attr: string, value?: unknown): R
+ /**
+ * @description
+ * Check whether the given element has certain classes within its `class` attribute.
+ *
+ * You must provide at least one class, unless you are asserting that an element does not have any classes.
+ * @example
+ *
+ *
+ *
no classes
+ *
+ * const deleteButton = getByTestId('delete-button')
+ * const noClasses = getByTestId('no-classes')
+ * expect(deleteButton).toHaveClass('btn')
+ * expect(deleteButton).toHaveClass('btn-danger xs')
+ * expect(deleteButton).toHaveClass(/danger/, 'xs')
+ * expect(deleteButton).toHaveClass('btn xs btn-danger', {exact: true})
+ * expect(deleteButton).not.toHaveClass('btn xs btn-danger', {exact: true})
+ * expect(noClasses).not.toHaveClass()
+ * @see
+ * [testing-library/jest-dom#tohaveclass](https://github.com/testing-library/jest-dom#tohaveclass)
+ */
+ toHaveClass(...classNames: Array): R
+ toHaveClass(classNames: string, options?: {exact: boolean}): R
+ /**
+ * @description
+ * This allows you to check whether the given form element has the specified displayed value (the one the
+ * end user will see). It accepts ,