diff --git a/.all-contributorsrc b/.all-contributorsrc index 05f6e72f..b61e4c41 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -126,6 +126,17 @@ "contributions": [ "code" ] + }, + { + "login": "gnapse", + "name": "Ernesto Garcรญa", + "avatar_url": "https://avatars0.githubusercontent.com/u/15199?v=4", + "profile": "http://gnapse.github.io", + "contributions": [ + "question", + "code", + "doc" + ] } ] } diff --git a/README.md b/README.md index 5d25e52b..9c22d936 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![downloads][downloads-badge]][npmtrends] [![MIT License][license-badge]][license] -[![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -82,7 +82,8 @@ facilitate testing implementation details). Read more about this in * [Custom Jest Matchers](#custom-jest-matchers) * [`toBeInTheDOM`](#tobeinthedom) * [`toHaveTextContent`](#tohavetextcontent) - * [Custom Jest Matchers - Typescript](#custom-jest-matchers-typescript) + * [`toHaveAttribute`](#tohaveattribute) + * [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Examples](#examples) @@ -138,6 +139,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl expect(axiosMock.get).toHaveBeenCalledTimes(1) expect(axiosMock.get).toHaveBeenCalledWith(url) expect(getByTestId('greeting-text')).toHaveTextContent('hello there') + expect(getByTestId('ok-button')).toHaveAttribute('disabled') // snapshots work great with regular DOM nodes! expect(container.firstChild).toMatchSnapshot() }) @@ -347,33 +349,55 @@ expect(getByTestId('count-value')).toHaveTextContent('2') expect(getByTestId('count-value')).not.toHaveTextContent('21') // ... ``` + +### `toHaveAttribute` + +This allows you to check wether the given element has an attribute or not. You +can also optionally check that the attribute has a specific expected value. + +```javascript +// add the custom expect matchers +import 'react-testing-library/extend-expect' + +// ... +const {getByTestId} = render( + , +) +expect(getByTestId('ok-button')).toHaveAttribute('disabled') +expect(getByTestId('ok-button')).toHaveAttribute('type', 'submit') +expect(getByTestId('ok-button')).not.toHaveAttribute('type', 'button') +// ... +``` + ### Custom Jest Matchers - Typescript -When you use custom Jest Matchers with Typescript, you will need to extend the type signature of `jest.Matchers`, then cast the result of `expect` accordingly. Here's a handy usage example: +When you use custom Jest Matchers with Typescript, you will need to extend the type signature of `jest.Matchers`, then cast the result of `expect` accordingly. Here's a handy usage example: ```typescript // this adds custom expect matchers -import 'react-testing-library/extend-expect'; +import 'react-testing-library/extend-expect' interface ExtendedMatchers extends jest.Matchers { - toHaveTextContent: (htmlElement: string) => object; - toBeInTheDOM: () => void; + toHaveTextContent: (htmlElement: string) => object + toBeInTheDOM: () => void } test('renders the tooltip as expected', async () => { const { // getByLabelText, getByText, // getByTestId, - container - } = render(Child); + container, + } = render(Child) // tests rendering of the child - getByText('Child'); + getByText('Child') // tests rendering of tooltip label - (expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent( - 'hello world' - ); + ;(expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent( + 'hello world', + ) // snapshots work great with regular DOM nodes! - expect(container.firstChild).toMatchSnapshot(); -}); + expect(container.firstChild).toMatchSnapshot() +}) ``` ## `TextMatch` @@ -715,10 +739,12 @@ light-weight, simple, and understandable. Thanks goes to these people ([emoji key][emojis]): + | [
Kent C. Dodds](https://kentcdodds.com)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [๐Ÿš‡](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [โš ๏ธ](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [
Ryan Castner](http://audiolion.github.io)
[๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [
Daniel Sandiego](https://www.dnlsandiego.com)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [
Paweล‚ Mikoล‚ajczyk](https://github.com/Miklet)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [
Alejandro ร‘รกรฑez Ortiz](http://co.linkedin.com/in/alejandronanez/)
[๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [
Matt Parrish](https://github.com/pbomb)
[๐Ÿ›](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Apbomb "Bug reports") [๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Code") [๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Documentation") [โš ๏ธ](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Tests") | [
Justin Hall](https://github.com/wKovacs64)
[๐Ÿ“ฆ](#platform-wKovacs64 "Packaging/porting to new platform") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| [
Anto Aravinth](https://github.com/antoaravinth)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Code") [โš ๏ธ](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Tests") [๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | [
ลukasz Gandecki](http://team.thebrain.pro)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Code") [โš ๏ธ](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Tests") [๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[๐Ÿ›](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [๐Ÿค”](#ideas-sompylasar "Ideas, Planning, & Feedback") | [
Jesse Day](https://github.com/jday3)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") | +| [
Anto Aravinth](https://github.com/antoaravinth)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Code") [โš ๏ธ](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Tests") [๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | [
ลukasz Gandecki](http://team.thebrain.pro)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Code") [โš ๏ธ](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Tests") [๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[๐Ÿ›](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [๐Ÿค”](#ideas-sompylasar "Ideas, Planning, & Feedback") | [
Jesse Day](https://github.com/jday3)
[๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") | [
Ernesto Garcรญa](http://gnapse.github.io)
[๐Ÿ’ฌ](#question-gnapse "Answering Questions") [๐Ÿ’ป](https://github.com/kentcdodds/react-testing-library/commits?author=gnapse "Code") [๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=gnapse "Documentation") | + This project follows the [all-contributors][all-contributors] specification. diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index f8fd4830..cf21ecd0 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -117,4 +117,34 @@ test('using jest helpers to assert element states', () => { ).toThrowError() }) +test('using jest helpers to check element attributes', () => { + const {queryByTestId} = render( + , + ) + + expect(queryByTestId('ok-button')).toHaveAttribute('disabled') + expect(queryByTestId('ok-button')).toHaveAttribute('type') + expect(queryByTestId('ok-button')).not.toHaveAttribute('class') + expect(queryByTestId('ok-button')).toHaveAttribute('type', 'submit') + expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'button') + + expect(() => + expect(queryByTestId('ok-button')).not.toHaveAttribute('disabled'), + ).toThrowError() + expect(() => + expect(queryByTestId('ok-button')).not.toHaveAttribute('type'), + ).toThrowError() + expect(() => + expect(queryByTestId('ok-button')).toHaveAttribute('class'), + ).toThrowError() + expect(() => + expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'submit'), + ).toThrowError() + expect(() => + expect(queryByTestId('ok-button')).toHaveAttribute('type', 'button'), + ).toThrowError() +}) + /* eslint jsx-a11y/label-has-for:0 */ diff --git a/src/extend-expect.js b/src/extend-expect.js index b2b99844..a2ddc2b4 100644 --- a/src/extend-expect.js +++ b/src/extend-expect.js @@ -1,4 +1,4 @@ import extensions from './jest-extensions' -const {toBeInTheDOM, toHaveTextContent} = extensions -expect.extend({toBeInTheDOM, toHaveTextContent}) +const {toBeInTheDOM, toHaveTextContent, toHaveAttribute} = extensions +expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute}) diff --git a/src/jest-extensions.js b/src/jest-extensions.js index c94e29f6..d0b74b8f 100644 --- a/src/jest-extensions.js +++ b/src/jest-extensions.js @@ -1,4 +1,12 @@ -import {matcherHint, printReceived, printExpected} from 'jest-matcher-utils' //eslint-disable-line import/no-extraneous-dependencies +//eslint-disable-next-line import/no-extraneous-dependencies +import { + matcherHint, + printReceived, + printExpected, + stringify, + RECEIVED_COLOR as receivedColor, + EXPECTED_COLOR as expectedColor, +} from 'jest-matcher-utils' import {matches} from './utils' function getDisplayName(subject) { @@ -9,10 +17,30 @@ function getDisplayName(subject) { } } +function checkHtmlElement(htmlElement) { + if (!(htmlElement instanceof HTMLElement)) { + throw new Error( + `The given subject is a ${getDisplayName( + htmlElement, + )}, not an HTMLElement`, + ) + } +} + const assertMessage = (assertionName, message, received, expected) => `${matcherHint(`${assertionName}`, 'received', '')} \n${message}: ` + `${printExpected(expected)} \nReceived: ${printReceived(received)}` +function printAttribute(name, value) { + return value === undefined ? name : `${name}=${stringify(value)}` +} + +function getAttributeComment(name, value) { + return value === undefined + ? `element.hasAttribute(${stringify(name)})` + : `element.getAttribute(${stringify(name)}) === ${stringify(value)}` +} + const extensions = { toBeInTheDOM(received) { getDisplayName(received) @@ -42,13 +70,7 @@ const extensions = { }, toHaveTextContent(htmlElement, checkWith) { - if (!(htmlElement instanceof HTMLElement)) - throw new Error( - `The given subject is a ${getDisplayName( - htmlElement, - )}, not an HTMLElement`, - ) - + checkHtmlElement(htmlElement) const textContent = htmlElement.textContent const pass = matches(textContent, htmlElement, checkWith) if (pass) { @@ -75,6 +97,41 @@ const extensions = { } } }, + + toHaveAttribute(htmlElement, name, expectedValue) { + checkHtmlElement(htmlElement) + const isExpectedValuePresent = expectedValue !== undefined + const hasAttribute = htmlElement.hasAttribute(name) + const receivedValue = htmlElement.getAttribute(name) + return { + pass: isExpectedValuePresent + ? hasAttribute && receivedValue === expectedValue + : hasAttribute, + message: () => { + const to = this.isNot ? 'not to' : 'to' + const receivedAttribute = receivedColor( + hasAttribute + ? printAttribute(name, receivedValue) + : 'attribute was not found', + ) + const expectedMsg = `Expected the element ${to} have attribute:\n ${expectedColor( + printAttribute(name, expectedValue), + )}` + const matcher = matcherHint( + `${this.isNot ? '.not' : ''}.toHaveAttribute`, + 'element', + printExpected(name), + { + secondArgument: isExpectedValuePresent + ? printExpected(expectedValue) + : undefined, + comment: getAttributeComment(name, expectedValue), + }, + ) + return `${matcher}\n\n${expectedMsg}\nReceived:\n ${receivedAttribute}` + }, + } + }, } export default extensions