diff --git a/README.md b/README.md
index d6f1f613..9762210e 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,8 @@ clear to read and to maintain.
- [`toBeVisible`](#tobevisible)
- [`toContainElement`](#tocontainelement)
- [`toContainHTML`](#tocontainhtml)
+ - [`toHaveAccessibleDescription`](#tohaveaccessibledescription)
+ - [`toHaveAccessibleName`](#tohaveaccessiblename)
- [`toHaveAttribute`](#tohaveattribute)
- [`toHaveClass`](#tohaveclass)
- [`toHaveFocus`](#tohavefocus)
@@ -155,8 +157,7 @@ toBeDisabled()
```
This allows you to check whether an element is disabled from the user's
-perspective.
-According to the specification, the following elements can be
+perspective. According to the specification, the following elements can be
[disabled](https://html.spec.whatwg.org/multipage/semantics-other.html#disabled-elements):
`button`, `input`, `select`, `textarea`, `optgroup`, `option`, `fieldset`.
@@ -526,6 +527,94 @@ expect(getByTestId('parent')).toContainHTML('')
+### `toHaveAccessibleDescription`
+
+```typescript
+toHaveAccessibleDescription(expectedAccessibleDescription?: string | RegExp)
+```
+
+This allows to assert that an element has the expected
+[accessible description](https://w3c.github.io/accname/).
+
+You can pass the exact string of the expected accessible description, or you can
+make a partial match passing a regular expression, or by using
+[expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring)/[expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp).
+
+#### Examples
+
+```html
+Start
+About
+
+
+The logo of Our Company
+```
+
+```js
+expect(getByTestId('link')).toHaveAccessibleDescription()
+expect(getByTestId('link')).toHaveAccessibleDescription('A link to start over')
+expect(getByTestId('link')).not.toHaveAccessibleDescription('Home page')
+expect(getByTestId('extra-link')).not.toHaveAccessibleDescription()
+expect(getByTestId('avatar')).not.toHaveAccessibleDescription()
+expect(getByTestId('logo')).not.toHaveAccessibleDescription('Company logo')
+expect(getByTestId('logo')).toHaveAccessibleDescription(
+ 'The logo of Our Company',
+)
+```
+
+
+
+### `toHaveAccessibleName`
+
+```typescript
+toHaveAccessibleName(expectedAccessibleName?: string | RegExp)
+```
+
+This allows to assert that an element is has the expected
+[accessible name](https://w3c.github.io/accname/). It is useful, for instance,
+to assert that form elements and buttons are properly labelled.
+
+You can pass the exact string of the expected accessible name, or you can make a
+partial match passing a regular expression, or by using
+[expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring)/[expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp).
+
+#### Examples
+
+```html
+
+
+Test title
+
+ Test content
+Test
+
+
+```
+
+```javascript
+expect(getByTestId('img-alt')).toHaveAccessibleName('Test alt')
+expect(getByTestId('img-empty-alt')).not.toHaveAccessibleName()
+expect(getByTestId('svg-title')).toHaveAccessibleName('Test title')
+expect(getByTestId('button-img-alt')).toHaveAccessibleName()
+expect(getByTestId('img-paragraph')).not.toHaveAccessibleName()
+expect(getByTestId('svg-button')).toHaveAccessibleName()
+expect(getByTestId('svg-without-title')).not.toHaveAccessibleName()
+expect(getByTestId('input-title')).toHaveAccessibleName()
+```
+
+
+
### `toHaveAttribute`
```typescript
diff --git a/package.json b/package.json
index 8a8e2466..816ace83 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"chalk": "^3.0.0",
"css": "^3.0.0",
"css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.5.6",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
diff --git a/src/__tests__/to-have-accessible-description.js b/src/__tests__/to-have-accessible-description.js
new file mode 100644
index 00000000..97527e52
--- /dev/null
+++ b/src/__tests__/to-have-accessible-description.js
@@ -0,0 +1,59 @@
+import {render} from './helpers/test-utils'
+
+describe('.toHaveAccessibleDescription', () => {
+ it('works with the link title attribute', () => {
+ const {queryByTestId} = render(`
+
+ `)
+
+ const link = queryByTestId('link')
+ expect(link).toHaveAccessibleDescription()
+ expect(link).toHaveAccessibleDescription('A link to start over')
+ expect(link).not.toHaveAccessibleDescription('Home page')
+ expect(() => {
+ expect(link).toHaveAccessibleDescription('Invalid description')
+ }).toThrow(/expected element to have accessible description/i)
+ expect(() => {
+ expect(link).not.toHaveAccessibleDescription()
+ }).toThrow(/expected element not to have accessible description/i)
+
+ const extraLink = queryByTestId('extra-link')
+ expect(extraLink).not.toHaveAccessibleDescription()
+ expect(() => {
+ expect(extraLink).toHaveAccessibleDescription()
+ }).toThrow(/expected element to have accessible description/i)
+ })
+
+ it('works with aria-describedby attributes', () => {
+ const {queryByTestId} = render(`
+
+
+
+
The logo of Our Company
+
+ `)
+
+ const avatar = queryByTestId('avatar')
+ expect(avatar).not.toHaveAccessibleDescription()
+ expect(() => {
+ expect(avatar).toHaveAccessibleDescription('User profile pic')
+ }).toThrow(/expected element to have accessible description/i)
+
+ const logo = queryByTestId('logo')
+ expect(logo).not.toHaveAccessibleDescription('Company logo')
+ expect(logo).toHaveAccessibleDescription('The logo of Our Company')
+ expect(logo).toHaveAccessibleDescription(/logo of our company/i)
+ expect(logo).toHaveAccessibleDescription(
+ expect.stringContaining('logo of Our Company'),
+ )
+ expect(() => {
+ expect(logo).toHaveAccessibleDescription("Our company's logo")
+ }).toThrow(/expected element to have accessible description/i)
+ expect(() => {
+ expect(logo).not.toHaveAccessibleDescription('The logo of Our Company')
+ }).toThrow(/expected element not to have accessible description/i)
+ })
+})
diff --git a/src/__tests__/to-have-accessible-name.js b/src/__tests__/to-have-accessible-name.js
new file mode 100644
index 00000000..b43cc85d
--- /dev/null
+++ b/src/__tests__/to-have-accessible-name.js
@@ -0,0 +1,318 @@
+import {render} from './helpers/test-utils'
+
+describe('.toHaveAccessibleName', () => {
+ it("recognizes an element's content as its label when appropriate", () => {
+ const {queryByTestId} = render(`
+
+
+ First element
+ Second element
+
+
+
+ Continue to the next step
+
+
+ `)
+
+ const list = queryByTestId('my-list')
+ expect(list).not.toHaveAccessibleName()
+ expect(() => {
+ expect(list).toHaveAccessibleName()
+ }).toThrow(/expected element to have accessible name/i)
+
+ expect(queryByTestId('first')).toHaveAccessibleName('First element')
+ expect(queryByTestId('second')).toHaveAccessibleName('Second element')
+
+ const button = queryByTestId('my-button')
+ expect(button).toHaveAccessibleName()
+ expect(button).toHaveAccessibleName('Continue to the next step')
+ expect(button).toHaveAccessibleName(/continue to the next step/i)
+ expect(button).toHaveAccessibleName(
+ expect.stringContaining('Continue to the next'),
+ )
+ expect(button).not.toHaveAccessibleName('Next step')
+ expect(() => {
+ expect(button).toHaveAccessibleName('Next step')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(button).not.toHaveAccessibleName('Continue to the next step')
+ }).toThrow(/expected element not to have accessible name/i)
+ expect(() => {
+ expect(button).not.toHaveAccessibleName()
+ }).toThrow(/expected element not to have accessible name/i)
+ })
+
+ it('works with label elements', () => {
+ const {queryByTestId} = render(`
+
+ First name
+
+
+
+
+ Accept terms and conditions
+
+
+ `)
+
+ const firstNameField = queryByTestId('first-name-field')
+ expect(firstNameField).toHaveAccessibleName('First name')
+ expect(queryByTestId('first-name-field')).toHaveAccessibleName(
+ /first name/i,
+ )
+ expect(firstNameField).toHaveAccessibleName(
+ expect.stringContaining('First'),
+ )
+ expect(() => {
+ expect(firstNameField).toHaveAccessibleName('Last name')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(firstNameField).not.toHaveAccessibleName('First name')
+ }).toThrow(/expected element not to have accessible name/i)
+
+ const checkboxField = queryByTestId('checkbox-field')
+ expect(checkboxField).toHaveAccessibleName('Accept terms and conditions')
+ expect(checkboxField).toHaveAccessibleName(/accept terms/i)
+ expect(checkboxField).toHaveAccessibleName(
+ expect.stringContaining('Accept terms'),
+ )
+ expect(() => {
+ expect(checkboxField).toHaveAccessibleName(
+ 'Accept our terms and conditions',
+ )
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(checkboxField).not.toHaveAccessibleName(
+ 'Accept terms and conditions',
+ )
+ }).toThrow(/expected element not to have accessible name/i)
+ })
+
+ it('works with aria-label attributes', () => {
+ const {queryByTestId} = render(`
+
+ First name
+
+
+
+
+ Accept terms and conditions
+
+
+
+ Continue
+
+
+ `)
+
+ const firstNameField = queryByTestId('first-name-field')
+ expect(firstNameField).not.toHaveAccessibleName('First name')
+ expect(firstNameField).toHaveAccessibleName('Enter your name')
+ expect(firstNameField).toHaveAccessibleName(/enter your name/i)
+ expect(firstNameField).toHaveAccessibleName(
+ expect.stringContaining('your name'),
+ )
+ expect(() => {
+ expect(firstNameField).toHaveAccessibleName('First name')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(firstNameField).not.toHaveAccessibleName('Enter your name')
+ }).toThrow(/expected element not to have accessible name/i)
+
+ const checkboxField = queryByTestId('checkbox-field')
+ expect(checkboxField).not.toHaveAccessibleName(
+ 'Accept terms and conditions',
+ )
+ expect(checkboxField).toHaveAccessibleName(
+ 'Accept our terms and conditions',
+ )
+ expect(checkboxField).toHaveAccessibleName(/accept our terms/i)
+ expect(checkboxField).toHaveAccessibleName(expect.stringContaining('terms'))
+ expect(() => {
+ expect(checkboxField).toHaveAccessibleName('Accept terms and conditions')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(checkboxField).not.toHaveAccessibleName(
+ 'Accept our terms and conditions',
+ )
+ }).toThrow(/expected element not to have accessible name/i)
+
+ const submitButton = queryByTestId('submit-button')
+ expect(submitButton).not.toHaveAccessibleName('Continue')
+ expect(submitButton).toHaveAccessibleName('Submit this form')
+ expect(submitButton).toHaveAccessibleName(/submit this form/i)
+ expect(submitButton).toHaveAccessibleName(expect.stringContaining('form'))
+ expect(() => {
+ expect(submitButton).toHaveAccessibleName('Continue')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(submitButton).not.toHaveAccessibleName('Submit this form')
+ }).toThrow(/expected element not to have accessible name/i)
+ })
+
+ it('works with aria-labelledby attributes', () => {
+ const {queryByTestId} = render(`
+
+
First name
+
+
Enter your name
+
+
+
+ Accept terms and conditions
+
+
Accept our terms and conditions
+
+
+ Continue
+
+
Submit this form
+
+ `)
+
+ const firstNameField = queryByTestId('first-name-field')
+ expect(firstNameField).not.toHaveAccessibleName('First name')
+ expect(firstNameField).toHaveAccessibleName('Enter your name')
+ expect(firstNameField).toHaveAccessibleName(/enter your name/i)
+ expect(firstNameField).toHaveAccessibleName(
+ expect.stringContaining('your name'),
+ )
+ expect(() => {
+ expect(firstNameField).toHaveAccessibleName('First name')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(firstNameField).not.toHaveAccessibleName('Enter your name')
+ }).toThrow(/expected element not to have accessible name/i)
+
+ const checkboxField = queryByTestId('checkbox-field')
+ expect(checkboxField).not.toHaveAccessibleName(
+ 'Accept terms and conditions',
+ )
+ expect(checkboxField).toHaveAccessibleName(
+ 'Accept our terms and conditions',
+ )
+ expect(checkboxField).toHaveAccessibleName(/accept our terms/i)
+ expect(checkboxField).toHaveAccessibleName(expect.stringContaining('terms'))
+ expect(() => {
+ expect(checkboxField).toHaveAccessibleName('Accept terms and conditions')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(checkboxField).not.toHaveAccessibleName(
+ 'Accept our terms and conditions',
+ )
+ }).toThrow(/expected element not to have accessible name/i)
+
+ const submitButton = queryByTestId('submit-button')
+ expect(submitButton).not.toHaveAccessibleName('Continue')
+ expect(submitButton).toHaveAccessibleName('Submit this form')
+ expect(submitButton).toHaveAccessibleName(/submit this form/i)
+ expect(submitButton).toHaveAccessibleName(expect.stringContaining('form'))
+ expect(() => {
+ expect(submitButton).toHaveAccessibleName('Continue')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(submitButton).not.toHaveAccessibleName('Submit this form')
+ }).toThrow(/expected element not to have accessible name/i)
+ })
+
+ it('works with image alt attributes', () => {
+ const {queryByTestId} = render(`
+
+
+
+
+
+
+ `)
+
+ const logoImage = queryByTestId('logo-img')
+ expect(logoImage).toHaveAccessibleName('Company logo')
+ expect(logoImage).toHaveAccessibleName(/company logo/i)
+ expect(logoImage).toHaveAccessibleName(expect.stringContaining('logo'))
+ expect(() => {
+ expect(logoImage).toHaveAccessibleName('Our company logo')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(logoImage).not.toHaveAccessibleName('Company logo')
+ }).toThrow(/expected element not to have accessible name/i)
+
+ const closeButton = queryByTestId('close-button')
+ expect(closeButton).toHaveAccessibleName('Close modal')
+ expect(closeButton).toHaveAccessibleName(/close modal/i)
+ expect(closeButton).toHaveAccessibleName(expect.stringContaining('modal'))
+ expect(() => {
+ expect(closeButton).toHaveAccessibleName('Close window')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(closeButton).not.toHaveAccessibleName('Close modal')
+ }).toThrow(/expected element not to have accessible name/i)
+ })
+
+ it('works with svg title attributes', () => {
+ const {queryByTestId} = render(`
+ Test title
+ `)
+
+ const svgElement = queryByTestId('svg-title')
+ expect(svgElement).toHaveAccessibleName('Test title')
+ expect(svgElement).toHaveAccessibleName(/test title/i)
+ expect(svgElement).toHaveAccessibleName(expect.stringContaining('Test'))
+ expect(() => {
+ expect(svgElement).toHaveAccessibleName('Another title')
+ }).toThrow(/expected element to have accessible name/i)
+ expect(() => {
+ expect(svgElement).not.toHaveAccessibleName('Test title')
+ }).toThrow(/expected element not to have accessible name/i)
+ })
+
+ it('works as in the examples in the README', () => {
+ const {queryByTestId: getByTestId} = render(`
+
+
+
+
Test title
+
+
Test content
+
Test
+
+
+
+ `)
+
+ expect(getByTestId('img-alt')).toHaveAccessibleName('Test alt')
+ expect(getByTestId('img-empty-alt')).not.toHaveAccessibleName()
+ expect(getByTestId('svg-title')).toHaveAccessibleName('Test title')
+ expect(getByTestId('button-img-alt')).toHaveAccessibleName()
+ expect(getByTestId('img-paragraph')).not.toHaveAccessibleName()
+ expect(getByTestId('svg-button')).toHaveAccessibleName()
+ expect(getByTestId('svg-without-title')).not.toHaveAccessibleName()
+ expect(getByTestId('input-title')).toHaveAccessibleName()
+ })
+})
diff --git a/src/matchers.js b/src/matchers.js
index 1dbbec77..c90945d5 100644
--- a/src/matchers.js
+++ b/src/matchers.js
@@ -5,6 +5,8 @@ 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'
@@ -29,6 +31,8 @@ export {
toContainElement,
toContainHTML,
toHaveTextContent,
+ toHaveAccessibleDescription,
+ toHaveAccessibleName,
toHaveAttribute,
toHaveClass,
toHaveStyle,
diff --git a/src/to-have-accessible-description.js b/src/to-have-accessible-description.js
new file mode 100644
index 00000000..8d77a10e
--- /dev/null
+++ b/src/to-have-accessible-description.js
@@ -0,0 +1,46 @@
+import {computeAccessibleDescription} from 'dom-accessibility-api'
+import {checkHtmlElement, getMessage} from './utils'
+
+export function toHaveAccessibleDescription(
+ htmlElement,
+ expectedAccessibleDescription,
+) {
+ checkHtmlElement(htmlElement, toHaveAccessibleDescription, this)
+ const actualAccessibleDescription = computeAccessibleDescription(htmlElement)
+ const missingExpectedValue = arguments.length === 1
+
+ let pass = false
+ if (missingExpectedValue) {
+ // When called without an expected value we only want to validate that the element has an
+ // accessible description, whatever it may be.
+ pass = actualAccessibleDescription !== ''
+ } else {
+ pass =
+ expectedAccessibleDescription instanceof RegExp
+ ? expectedAccessibleDescription.test(actualAccessibleDescription)
+ : this.equals(
+ actualAccessibleDescription,
+ expectedAccessibleDescription,
+ )
+ }
+
+ return {
+ pass,
+
+ message: () => {
+ const to = this.isNot ? 'not to' : 'to'
+ return getMessage(
+ this,
+ this.utils.matcherHint(
+ `${this.isNot ? '.not' : ''}.${toHaveAccessibleDescription.name}`,
+ 'element',
+ '',
+ ),
+ `Expected element ${to} have accessible description`,
+ expectedAccessibleDescription,
+ 'Received',
+ actualAccessibleDescription,
+ )
+ },
+ }
+}
diff --git a/src/to-have-accessible-name.js b/src/to-have-accessible-name.js
new file mode 100644
index 00000000..9cd2f39b
--- /dev/null
+++ b/src/to-have-accessible-name.js
@@ -0,0 +1,40 @@
+import {computeAccessibleName} from 'dom-accessibility-api'
+import {checkHtmlElement, getMessage} from './utils'
+
+export function toHaveAccessibleName(htmlElement, expectedAccessibleName) {
+ checkHtmlElement(htmlElement, toHaveAccessibleName, this)
+ const actualAccessibleName = computeAccessibleName(htmlElement)
+ const missingExpectedValue = arguments.length === 1
+
+ let pass = false
+ if (missingExpectedValue) {
+ // When called without an expected value we only want to validate that the element has an
+ // accessible name, whatever it may be.
+ pass = actualAccessibleName !== ''
+ } else {
+ pass =
+ expectedAccessibleName instanceof RegExp
+ ? expectedAccessibleName.test(actualAccessibleName)
+ : this.equals(actualAccessibleName, expectedAccessibleName)
+ }
+
+ return {
+ pass,
+
+ message: () => {
+ const to = this.isNot ? 'not to' : 'to'
+ return getMessage(
+ this,
+ this.utils.matcherHint(
+ `${this.isNot ? '.not' : ''}.${toHaveAccessibleName.name}`,
+ 'element',
+ '',
+ ),
+ `Expected element ${to} have accessible name`,
+ expectedAccessibleName,
+ 'Received',
+ actualAccessibleName,
+ )
+ },
+ }
+}