From 0618715b40d63b2c3d0b52c5384bb7016a619c7d Mon Sep 17 00:00:00 2001 From: Ernesto Garcia Date: Thu, 5 Apr 2018 10:35:16 -0300 Subject: [PATCH 1/5] Add toHaveAttribute custom matcher --- README.md | 52 ++++++++++++++++++++++-------- src/extend-expect.js | 4 +-- src/jest-extensions.js | 73 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 106 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5d25e52b..3a639e4c 100644 --- a/README.md +++ b/README.md @@ -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") | + This project follows the [all-contributors][all-contributors] specification. 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..64333bc3 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,29 @@ 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 getAttrReceivedMsg(hasAttribute, expectedValue, receivedValue) { + if (!hasAttribute) { + return receivedColor('the attribute was not found') + } + return expectedValue === undefined + ? receivedColor('the attribute was present') + : `got ${receivedColor(`${name}=${stringify(receivedValue)}`)}` +} + const extensions = { toBeInTheDOM(received) { getDisplayName(received) @@ -42,13 +69,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 +96,42 @@ 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 secondArgument = isExpectedValuePresent + ? printExpected(expectedValue) + : undefined + const to = this.isNot ? 'not to' : 'to' + const expectedAttribute = isExpectedValuePresent + ? expectedColor(`${name}=${stringify(expectedValue)}`) + : expectedColor(name) + const expectedMsg = `Expected the element ${to} have attribute ${expectedAttribute}` + const receivedMsg = getAttrReceivedMsg( + hasAttribute, + expectedValue, + receivedValue, + ) + const matcher = matcherHint( + `${this.isNot ? '.not' : ''}.toHaveAttribute`, + 'element', + printExpected(name), + { + secondArgument, + }, + ) + return `${matcher}\n\n${expectedMsg}\nInstead ${receivedMsg}` + }, + } + }, } export default extensions From bfeb26aa29e2b36cbf254a6cb8dc6934e32a055e Mon Sep 17 00:00:00 2001 From: Ernesto Garcia Date: Thu, 5 Apr 2018 10:45:34 -0300 Subject: [PATCH 2/5] Add gnapse as contributor --- .all-contributorsrc | 11 +++++++++++ README.md | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) 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 3a639e4c..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] @@ -743,7 +743,7 @@ 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") | From 12db31268242ad819bda9c318359cf1d8c07857b Mon Sep 17 00:00:00 2001 From: Ernesto Garcia Date: Thu, 5 Apr 2018 17:01:13 -0300 Subject: [PATCH 3/5] Generate messages more consistent with jest's other matchers --- src/jest-extensions.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/jest-extensions.js b/src/jest-extensions.js index 64333bc3..53d75bde 100644 --- a/src/jest-extensions.js +++ b/src/jest-extensions.js @@ -31,13 +31,8 @@ const assertMessage = (assertionName, message, received, expected) => `${matcherHint(`${assertionName}`, 'received', '')} \n${message}: ` + `${printExpected(expected)} \nReceived: ${printReceived(received)}` -function getAttrReceivedMsg(hasAttribute, expectedValue, receivedValue) { - if (!hasAttribute) { - return receivedColor('the attribute was not found') - } - return expectedValue === undefined - ? receivedColor('the attribute was present') - : `got ${receivedColor(`${name}=${stringify(receivedValue)}`)}` +function printAttribute(name, value) { + return value === undefined ? name : `${name}=${stringify(value)}` } const extensions = { @@ -99,7 +94,7 @@ const extensions = { toHaveAttribute(htmlElement, name, expectedValue) { checkHtmlElement(htmlElement) - const isExpectedValuePresent = expectedValue === undefined + const isExpectedValuePresent = expectedValue !== undefined const hasAttribute = htmlElement.hasAttribute(name) const receivedValue = htmlElement.getAttribute(name) return { @@ -111,15 +106,14 @@ const extensions = { ? printExpected(expectedValue) : undefined const to = this.isNot ? 'not to' : 'to' - const expectedAttribute = isExpectedValuePresent - ? expectedColor(`${name}=${stringify(expectedValue)}`) - : expectedColor(name) - const expectedMsg = `Expected the element ${to} have attribute ${expectedAttribute}` - const receivedMsg = getAttrReceivedMsg( - hasAttribute, - expectedValue, - receivedValue, + 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', @@ -128,7 +122,7 @@ const extensions = { secondArgument, }, ) - return `${matcher}\n\n${expectedMsg}\nInstead ${receivedMsg}` + return `${matcher}\n\n${expectedMsg}\nReceived:\n ${receivedAttribute}` }, } }, From aaf03ae7fe12634166cd7d4cb5137b3aa598195d Mon Sep 17 00:00:00 2001 From: Ernesto Garcia Date: Thu, 5 Apr 2018 17:24:43 -0300 Subject: [PATCH 4/5] Add comment to matcher hint --- src/jest-extensions.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/jest-extensions.js b/src/jest-extensions.js index 53d75bde..d0b74b8f 100644 --- a/src/jest-extensions.js +++ b/src/jest-extensions.js @@ -35,6 +35,12 @@ 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) @@ -102,9 +108,6 @@ const extensions = { ? hasAttribute && receivedValue === expectedValue : hasAttribute, message: () => { - const secondArgument = isExpectedValuePresent - ? printExpected(expectedValue) - : undefined const to = this.isNot ? 'not to' : 'to' const receivedAttribute = receivedColor( hasAttribute @@ -119,7 +122,10 @@ const extensions = { 'element', printExpected(name), { - secondArgument, + secondArgument: isExpectedValuePresent + ? printExpected(expectedValue) + : undefined, + comment: getAttributeComment(name, expectedValue), }, ) return `${matcher}\n\n${expectedMsg}\nReceived:\n ${receivedAttribute}` From c1910084bb96604f27cb133136cf1b893fe5f945 Mon Sep 17 00:00:00 2001 From: Ernesto Garcia Date: Thu, 5 Apr 2018 18:02:48 -0300 Subject: [PATCH 5/5] Add tests to cover toHaveAttribute custom matcher --- src/__tests__/element-queries.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) 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 */