diff --git a/README.md b/README.md
index 1778f8fe..af3d2492 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ clear to read and to maintain.
- [`toHaveAccessibleDescription`](#tohaveaccessibledescription)
- [`toHaveAccessibleName`](#tohaveaccessiblename)
- [`toHaveAttribute`](#tohaveattribute)
+ - [`toHaveAttributes`](#tohaveattributes)
- [`toHaveClass`](#tohaveclass)
- [`toHaveFocus`](#tohavefocus)
- [`toHaveFormValues`](#tohaveformvalues)
@@ -631,6 +632,41 @@ expect(button).toHaveAttribute('type', expect.not.stringContaining('but'))
+### `toHaveAttributes`
+
+```typescript
+toHaveAttributes({[attr: string]: any})
+```
+
+This allows you to check whether the given element has attributes or not. You
+can also optionally check that the attributes have specific expected values or
+partial match using
+[expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring)/[expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp)
+
+#### Examples
+
+```html
+
+```
+
+```javascript
+const button = getByTestId('ok-button')
+
+expect(button).toHaveAttributes({type: 'submit', disabled: ''})
+expect(button).not.toHaveAttributes({type: 'button', disabled: 'false'})
+
+expect(button).toHaveAttributes({
+ type: expect.stringContaining('sub'),
+ disabled: '',
+})
+expect(button).toHaveAttributes({
+ type: expect.not.stringContaining('but'),
+ disabled: 'false',
+})
+```
+
+
+
### `toHaveClass`
```typescript
diff --git a/src/__tests__/to-have-attributes.js b/src/__tests__/to-have-attributes.js
new file mode 100644
index 00000000..c9055ec4
--- /dev/null
+++ b/src/__tests__/to-have-attributes.js
@@ -0,0 +1,169 @@
+import {render} from './helpers/test-utils'
+
+const invalidDataErrorBase =
+ '.toHaveAttributes() expects object with at least one property, received'
+
+test('.toHaveAttributes', () => {
+ const {queryByTestId} = render(`
+
+
+ `)
+
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ disabled: '',
+ type: 'submit',
+ 'data-testid': 'ok-button',
+ })
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ disabled: false,
+ type: 'reset',
+ 'data-testid': 'ok',
+ })
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ disabled: 'false',
+ type: 'reset',
+ 'data-testid': 'ok',
+ })
+
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: 'submit',
+ })
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: 'reset',
+ })
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: '',
+ })
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: true,
+ })
+
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ disabled: '',
+ })
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ disabled: 'true',
+ })
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ disabled: true,
+ })
+
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ value: 'value',
+ })
+
+ expect(queryByTestId('svg-element')).toHaveAttributes({width: '12'})
+ expect(queryByTestId('svg-element')).not.toHaveAttributes({width: '13'})
+ expect(queryByTestId('svg-element')).not.toHaveAttributes({height: '12'})
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).toHaveAttributes(),
+ ).toThrowError(`${invalidDataErrorBase} undefined`)
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes(),
+ ).toThrowError(`${invalidDataErrorBase} undefined`)
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).toHaveAttributes({}),
+ ).toThrowError(`${invalidDataErrorBase} {}`)
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({}),
+ ).toThrowError(`${invalidDataErrorBase} {}`)
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).toHaveAttributes('disabled'),
+ ).toThrowError(`${invalidDataErrorBase} "disabled"`)
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes('disabled'),
+ ).toThrowError(`${invalidDataErrorBase} "disabled"`)
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).toHaveAttributes(true),
+ ).toThrowError(`${invalidDataErrorBase} true`)
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes(true),
+ ).toThrowError(`${invalidDataErrorBase} true`)
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).toHaveAttributes(false),
+ ).toThrowError(`${invalidDataErrorBase} false`)
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes(false),
+ ).toThrowError(`${invalidDataErrorBase} false`)
+
+ // // Asymmetric matchers
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: expect.stringContaining('sub'),
+ disabled: '',
+ })
+
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: expect.not.stringContaining('res'),
+ disabled: '',
+ })
+
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: expect.anything(),
+ disabled: '',
+ })
+
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: expect.stringMatching(/sub*/),
+ disabled: '',
+ })
+
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: expect.not.stringMatching(/res*/),
+ disabled: '',
+ })
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: expect.stringContaining('sub'),
+ }),
+ ).toThrowError()
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: expect.stringContaining('sub'),
+ disabled: '',
+ }),
+ ).toThrowError()
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: expect.not.stringContaining('sub'),
+ disabled: '',
+ }),
+ ).toThrowError()
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: expect.stringContaining('res'),
+ disabled: '',
+ }),
+ ).toThrowError()
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).toHaveAttributes({
+ type: expect.stringContaining('res'),
+ disabled: '',
+ }),
+ ).toThrowError()
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: expect.anything(),
+ disabled: '',
+ }),
+ ).toThrowError()
+
+ expect(() =>
+ expect(queryByTestId('ok-button')).not.toHaveAttributes({
+ type: expect.stringMatching(/sub*/),
+ disabled: '',
+ }),
+ ).toThrowError()
+})
diff --git a/src/matchers.js b/src/matchers.js
index c90945d5..2df42895 100644
--- a/src/matchers.js
+++ b/src/matchers.js
@@ -8,6 +8,7 @@ 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 {toHaveAttributes} from './to-have-attributes'
import {toHaveClass} from './to-have-class'
import {toHaveStyle} from './to-have-style'
import {toHaveFocus} from './to-have-focus'
@@ -34,6 +35,7 @@ export {
toHaveAccessibleDescription,
toHaveAccessibleName,
toHaveAttribute,
+ toHaveAttributes,
toHaveClass,
toHaveStyle,
toHaveFocus,
diff --git a/src/to-have-attributes.js b/src/to-have-attributes.js
new file mode 100644
index 00000000..c4249a8a
--- /dev/null
+++ b/src/to-have-attributes.js
@@ -0,0 +1,70 @@
+import {size} from 'lodash'
+import {checkHtmlElement} from './utils'
+
+export function toHaveAttributes(htmlElement, expectedAttributes) {
+ checkHtmlElement(htmlElement, toHaveAttributes, this)
+ const hasExpectedAttributes =
+ typeof expectedAttributes === 'object' && !!size(expectedAttributes)
+
+ const receivedAttributes = {}
+ let isValid = hasExpectedAttributes
+
+ if (hasExpectedAttributes) {
+ Object.entries(expectedAttributes).forEach(
+ ([expectedAttribute, expectedValue]) => {
+ const hasAttribute = htmlElement.hasAttribute(expectedAttribute)
+ const receivedValue = htmlElement.getAttribute(expectedAttribute)
+ const areValuesEqual = this.equals(receivedValue, expectedValue)
+
+ if (!this.isNot) {
+ if (!areValuesEqual) {
+ isValid = false
+ }
+
+ if (hasAttribute) {
+ receivedAttributes[expectedAttribute] = receivedValue
+ }
+ } else if (areValuesEqual) {
+ isValid = false
+ if (hasAttribute) {
+ receivedAttributes[expectedAttribute] = receivedValue
+ }
+ }
+ },
+ )
+ }
+
+ const pass = hasExpectedAttributes && isValid
+
+ return {
+ pass: this.isNot ? !pass : pass,
+ message: () => {
+ const matcher = hasExpectedAttributes
+ ? this.utils.matcherHint(
+ `${this.isNot ? '.not' : ''}.toHaveAttributes`,
+ 'received',
+ 'expected',
+ )
+ : undefined
+
+ const message = hasExpectedAttributes
+ ? this.utils.diff(expectedAttributes, receivedAttributes, {
+ includeChangeCounts: true,
+ })
+ : // eslint-disable-next-line @babel/new-cap
+ this.utils.RECEIVED_COLOR(
+ `.toHaveAttributes() expects object with at least one property, received ${this.utils.stringify(
+ expectedAttributes,
+ )}`,
+ )
+
+ const to = this.isNot ? 'not to' : 'to'
+
+ return [
+ matcher,
+ `Expected the element ${to} have attributes`,
+ message,
+ ].join('\n\n')
+ },
+ }
+}