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') + }, + } +}