From 8403e84708154a59f49d423bd29f9933b94b02e0 Mon Sep 17 00:00:00 2001 From: Jonah Moses Date: Tue, 27 Mar 2018 14:22:37 -0700 Subject: [PATCH 001/506] docs: update readme for getByLabelText (#24) --- .all-contributorsrc | 9 +++++++++ README.md | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9b34a8ad..1dc8d820 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -76,6 +76,15 @@ "contributions": [ "platform" ] + }, + { + "login": "JonahMoses", + "name": "Jonah Moses", + "avatar_url": "https://avatars2.githubusercontent.com/u/3462296?v=4", + "profile": "https://github.com/JonahMoses", + "contributions": [ + "doc" + ] } ] } diff --git a/README.md b/README.md index 41f85a7d..cfe56d0f 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-7-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -171,7 +171,7 @@ const inputNode = getByLabelText('Username') // // // For this case, you can provide a `selector` in the options: -const inputNode = getByLabelText('username-input', {selector: 'input'}) +const inputNode = getByLabelText('username', {selector: 'input'}) // and that would work ``` @@ -564,6 +564,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") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| [
Jonah Moses](https://github.com/JonahMoses)
[๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | From 50aa1bce46b7a5fff287920266a9c9fd2ad880cf Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Wed, 28 Mar 2018 07:45:07 -0600 Subject: [PATCH 002/506] docs(problem-solution): update problem/solution --- README.md | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cfe56d0f..3b081e97 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,47 @@ ## The problem -You want to write maintainable tests for your React components. However, the -de facto standard for testing ([enzyme](https://github.com/airbnb/enzyme)) is -bloated with complexity and features, most of which encourage poor testing -practices (mostly relating to testing implementation details). +You want to write maintainable tests for your React components. As a part of +this goal, you want your tests to avoid including implementation details of +your components and rather focus on making your tests give you the confidence +for which they are intended. As part of this, you want your testbase to be +maintainable in the long run so refactors of you components (changes to +implementation but not functionality) don't break your tests and slow you and +your team down. ## This solution The `react-testing-library` is a very light-weight solution for testing React components. It provides light utility functions on top of `react-dom` and `react-dom/test-utils`, in a way that encourages better testing practices. +It's primary guiding principle is: + +> [The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle] + +So rather than dealing with instances of rendered react components, your tests +will work with actual DOM nodes. The utilities this library provides facilitate +querying the DOM in the same way the user would. Finding for elements by their +label text (just like a user would), finding links and buttons from their text +(like a user would). It also exposes a recommended way to find elements by a +`data-testid` as an "escape hatch" for elements where the text content and label +do not make sense or is not practical. + +This library encourages your applications to be more accessible and allows you +to get your tests closer to using your components the way a user will, which +allows your tests to give you more confidence that your application will work +when a real user uses it. + +This library is a replacement for [enzyme](http://airbnb.io/enzyme/). While you +_can_ follow these guidlines using enzyme itself, enforcing this is harder +because of all the extra utilities that enzyme provides (utilities which +facilitate testing implementation details). Read more about this in +[the FAQ](#faq) below. + +**What this library is not**: + +1. A test runner or framework +2. Specific to a testing framework (though we recommend Jest as our + preference, the library works with any framework) ## Table of Contents From 18104fe3a2b38770d83b6a204150436dc32b6aa5 Mon Sep 17 00:00:00 2001 From: ants Date: Wed, 28 Mar 2018 20:58:33 +0530 Subject: [PATCH 003/506] feat(matchers): add custom jest matchers (#13) * Improving API's for testing. * Improving Apis * Adding all contributions * Fixing wrong url * Fixing review comments & making colorful assertions :) * Removing unwanted changes * Fixing review comments * removing unwanted comments * Adding test cases for the coverage * removing commented code and making few changes to the contribution file * Updating the readme * Making line break changes * Update README.md --- .all-contributorsrc | 11 +++++ README.md | 43 +++++++++++++++++- extend-expect.js | 1 + package.json | 5 +- src/__tests__/element-queries.js | 14 ++++++ src/__tests__/fetch.js | 2 +- src/extend-expect.js | 7 +++ src/jest-extensions.js | 78 ++++++++++++++++++++++++++++++++ src/queries.js | 12 +---- src/utils.js | 10 ++++ 10 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 extend-expect.js create mode 100644 src/extend-expect.js create mode 100644 src/jest-extensions.js create mode 100644 src/utils.js diff --git a/.all-contributorsrc b/.all-contributorsrc index 1dc8d820..91f0d2cd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -77,6 +77,17 @@ "platform" ] }, + { + "login": "antoaravinth", + "name": "Anto Aravinth", + "avatar_url": "https://avatars1.githubusercontent.com/u/1241511?s=460&v=4", + "profile": "https://github.com/antoaravinth", + "contributions": [ + "code", + "test", + "doc" + ] + }, { "login": "JonahMoses", "name": "Jonah Moses", diff --git a/README.md b/README.md index 3b081e97..6e3e7ecb 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-8-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -79,6 +79,7 @@ facilitate testing implementation details). Read more about this in * [`Simulate`](#simulate) * [`flushPromises`](#flushpromises) * [`render`](#render) +* [Custom Jest Matchers](#custom-jest-matchers) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Examples](#examples) @@ -250,6 +251,44 @@ const usernameInputElement = getByTestId('username-input') > Learn more about `data-testid`s from the blog post > ["Making your UI tests resilient to change"][data-testid-blog-post] +## Custom Jest Matchers + +There are two simple API which extend the `expect` API of jest for making assertions easier. + +### `toBeInTheDOM` + +This allows you to assert whether an element present in the DOM or not. + +```javascript +// add the custom expect matchers +import 'react-testing-library/extend-expect' + +// ... +const {queryByTestId} = render(2) +expect(queryByTestId('count-value')).toBeInTheDOM() +expect(queryByTestId('count-value1')).not.toBeInTheDOM() +// ... +``` + +> Note: when using `toBeInTheDOM`, make sure you use a query function +> (like `queryByTestId`) rather than a get function (like `getByTestId`). +> Otherwise the `get*` function could throw an error before your assertion. + +### `toHaveTextContent` + +This API allows you to check whether the given element has a text content or not. + +```javascript +// add the custom expect matchers +import 'react-testing-library/extend-expect' + +// ... +const {getByTestId} = render(2) +expect(getByTestId('count-value')).toHaveTextContent('2') +expect(getByTestId('count-value')).not.toHaveTextContent('21') +// ... +``` + ## `TextMatch` Several APIs accept a `TextMatch` which can be a `string`, `regex` or a @@ -595,7 +634,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") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| [
Jonah Moses](https://github.com/JonahMoses)
[๐Ÿ“–](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | +| [
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") | diff --git a/extend-expect.js b/extend-expect.js new file mode 100644 index 00000000..3cee4049 --- /dev/null +++ b/extend-expect.js @@ -0,0 +1 @@ +require('./dist/extend-expect') diff --git a/package.json b/package.json index 06c161a7..d709aae9 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", "rules": { - "react/prop-types": "off" + "react/prop-types": "off", + "import/no-unassigned-import": "off" } }, "eslintIgnore": [ @@ -61,4 +62,4 @@ "url": "https://github.com/kentcdodds/react-testing-library/issues" }, "homepage": "https://github.com/kentcdodds/react-testing-library#readme" -} +} \ No newline at end of file diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index cec3f1ab..a5912c8d 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -1,5 +1,6 @@ import React from 'react' import {render} from '../' +import '../extend-expect' test('query can return null', () => { const { @@ -66,4 +67,17 @@ test('totally empty label', () => { expect(() => getByLabelText('')).toThrowErrorMatchingSnapshot() }) +test('using jest helpers to assert element states', () => { + const {queryByTestId} = render(2) + + // other ways to assert your test cases, but you don't need all of them. + expect(queryByTestId('count-value')).toBeInTheDOM() + expect(queryByTestId('count-value1')).not.toBeInTheDOM() + expect(queryByTestId('count-value')).toHaveTextContent('2') + expect(queryByTestId('count-value')).not.toHaveTextContent('21') + expect(() => + expect(queryByTestId('count-value2')).toHaveTextContent('2'), + ).toThrowError() +}) + /* eslint jsx-a11y/label-has-for:0 */ diff --git a/src/__tests__/fetch.js b/src/__tests__/fetch.js index c20c7fbc..bddb1489 100644 --- a/src/__tests__/fetch.js +++ b/src/__tests__/fetch.js @@ -35,7 +35,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl }), ) const url = '/greeting' - const {getByText, container} = render() + const {container, getByText} = render() // Act Simulate.click(getByText('Fetch')) diff --git a/src/extend-expect.js b/src/extend-expect.js new file mode 100644 index 00000000..58359822 --- /dev/null +++ b/src/extend-expect.js @@ -0,0 +1,7 @@ +import expect from 'expect' //eslint-disable-line import/no-extraneous-dependencies +import extensions from './jest-extensions' + +const {toBeInTheDOM, toHaveTextContent} = extensions +expect.extend({toBeInTheDOM, toHaveTextContent}) + +export default expect diff --git a/src/jest-extensions.js b/src/jest-extensions.js new file mode 100644 index 00000000..69e1e92c --- /dev/null +++ b/src/jest-extensions.js @@ -0,0 +1,78 @@ +import {matcherHint, printReceived, printExpected} from 'jest-matcher-utils' //eslint-disable-line import/no-extraneous-dependencies +import {matches} from './utils' + +function getDisplayName(subject) { + if (subject && subject.constructor) { + return subject.constructor.name + } else { + return typeof subject + } +} + +const assertMessage = (assertionName, message, received, expected) => + `${matcherHint(`${assertionName}`, 'received', '')} \n${message}: ` + + `${printExpected(expected)} \nReceived: ${printReceived(received)}` + +const extensions = { + toBeInTheDOM(received) { + getDisplayName(received) + if (received) { + return { + message: + `${matcherHint( + '.not.toBeInTheDOM', + 'received', + '', + )} Expected the element not to be present` + + `\nReceived : ${printReceived(received)}`, + pass: true, + } + } else { + return { + message: + `${matcherHint( + '.toBeInTheDOM', + 'received', + '', + )} Expected the element to be present` + + `\nReceived : ${printReceived(received)}`, + pass: false, + } + } + }, + + toHaveTextContent(htmlElement, checkWith) { + if (!(htmlElement instanceof HTMLElement)) + throw new Error( + `The given subject is a ${getDisplayName( + htmlElement, + )}, not an HTMLElement`, + ) + + const textContent = htmlElement.textContent + const pass = matches(textContent, htmlElement, checkWith) + if (pass) { + return { + message: assertMessage( + '.not.toHaveTextContent', + 'Expected value not equals to', + htmlElement, + checkWith, + ), + pass: true, + } + } else { + return { + message: assertMessage( + '.toHaveTextContent', + 'Expected value equals to', + htmlElement, + checkWith, + ), + pass: false, + } + } + }, +} + +export default extensions diff --git a/src/queries.js b/src/queries.js index 94382dab..485e2826 100644 --- a/src/queries.js +++ b/src/queries.js @@ -1,3 +1,5 @@ +import {matches} from './utils' + // Here are the queries for the library. // The queries here should only be things that are accessible to both users who are using a screen reader // and those who are not using a screen reader (with the exception of the data-testid attribute query). @@ -69,16 +71,6 @@ function getText(node) { .join(' ') } -function matches(textToMatch, node, matcher) { - if (typeof matcher === 'string') { - return textToMatch.toLowerCase().includes(matcher.toLowerCase()) - } else if (typeof matcher === 'function') { - return matcher(textToMatch, node) - } else { - return matcher.test(textToMatch) - } -} - // getters // the reason we're not dynamically generating these functions that look so similar: // 1. The error messages are specific to each one and depend on arguments diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 00000000..87ecef8b --- /dev/null +++ b/src/utils.js @@ -0,0 +1,10 @@ +//eslint-disable-next-line import/prefer-default-export +export function matches(textToMatch, node, matcher) { + if (typeof matcher === 'string') { + return textToMatch.toLowerCase().includes(matcher.toLowerCase()) + } else if (typeof matcher === 'function') { + return matcher(textToMatch, node) + } else { + return matcher.test(textToMatch) + } +} From 66e8d45ace4a4485191f0c69231b6883d06c4d3f Mon Sep 17 00:00:00 2001 From: Stephen Mathieson Date: Thu, 29 Mar 2018 00:26:59 -0400 Subject: [PATCH 004/506] docs: fix you->your typo (#26) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e3e7ecb..d1809fef 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ You want to write maintainable tests for your React components. As a part of this goal, you want your tests to avoid including implementation details of your components and rather focus on making your tests give you the confidence for which they are intended. As part of this, you want your testbase to be -maintainable in the long run so refactors of you components (changes to +maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down. From b13b1c142125bdba152f0fd5fe9fff7f1bafb96b Mon Sep 17 00:00:00 2001 From: ants Date: Thu, 29 Mar 2018 20:53:45 +0530 Subject: [PATCH 005/506] fix: add the missed file `"extend-expect.js"` to `package.json` (#28) --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d709aae9..77d2e484 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ }, "files": [ "dist", - "typings" + "typings", + "extend-expect.js" ], "keywords": [], "author": "Kent C. Dodds (http://kentcdodds.com/)", @@ -62,4 +63,4 @@ "url": "https://github.com/kentcdodds/react-testing-library/issues" }, "homepage": "https://github.com/kentcdodds/react-testing-library#readme" -} \ No newline at end of file +} From 4adbc1daa59ccc2c7ccef0b0461c1f7fcaab1526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Gandecki?= Date: Thu, 29 Mar 2018 19:34:49 +0400 Subject: [PATCH 006/506] feat(waitForExpect): add waitForExpect (#25) * added waitForExpect with test * added typescript and simplified version of the waitForExpect, used and exports its typings * added initial notes about waitForExpect * fixed styling * minor stylistic change * updated tests to remove the nesting * updated readme * fixed d .md syntax * improved style Closes #21 --- .all-contributorsrc | 10 ++++++ README.md | 66 +++++++++++++++++++++++++++++++++++-- package.json | 4 ++- src/__tests__/end-to-end.js | 40 ++++++++++++++++++++++ src/index.js | 3 +- typings/index.d.ts | 3 ++ 6 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/end-to-end.js diff --git a/.all-contributorsrc b/.all-contributorsrc index 91f0d2cd..5bd7184b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -96,6 +96,16 @@ "contributions": [ "doc" ] + }, + { + "login": "lgandecki", + "name": "ลukasz Gandecki", + "avatar_url": "https://avatars1.githubusercontent.com/u/4002543?v=4", + "profile": "http://team.thebrain.pro", + "contributions": [ + "code", + "test" + ] } ] } diff --git a/README.md b/README.md index d1809fef..3627c8ac 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-9-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -78,8 +78,11 @@ facilitate testing implementation details). Read more about this in * [Usage](#usage) * [`Simulate`](#simulate) * [`flushPromises`](#flushpromises) + * [`waitForExpect`](#waitforexpect) * [`render`](#render) * [Custom Jest Matchers](#custom-jest-matchers) + * [`toBeInTheDOM`](#tobeinthedom) + * [`toHaveTextContent`](#tohavetextcontent) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Examples](#examples) @@ -151,6 +154,35 @@ you make your test function an `async` function and use See an example in the section about `render` below. +### `waitForExpect` + +Defined as: + +```javascript +waitForExpect(expectation: () => void, timeout?: number, interval?: number) => Promise<{}>; +``` + +When in need to wait for non-deterministic periods of time you can use waitForExpect, +to wait for your expectations to pass. Take a look at [`Is there a different way to wait for things to happen?`](#waitForExpect) part of the FAQ, +or the function documentation here: [`wait-for-expect`](https://github.com/TheBrainFamily/wait-for-expect) +or just take a look at this simple example: + +```javascript +... +await waitForExpect(() => expect(queryByLabelText('username')).not.toBeNull()) +getByLabelText('username').value = 'chucknorris' +... +``` + +Another advantage of waitForExpect in comparison to flushPromises, is that +flushPromises will not flush promises that have not been queued up already, +for example, if they will queue up as a result of the initial promises. +In consequence of that, you might have to call flushPromises multiple times to get your components +to your desired state. + +This can happen for example, when you integration test your apollo-connected react components +that go a couple level deep, with queries fired up in consequent components. + ### `render` In the example above, the `render` method returns an object that has a few @@ -591,6 +623,36 @@ that this is only effective if you've mocked out your async requests to resolve immediately (like the `axios` mock we have in the examples). It will not `await` for promises that are not already resolved by the time you attempt to flush them. +In case this doesn't work for you the way you would expect, take a look at the +waitForExpect function that should be much more intuitive to use. + + + +
+ +Is there a different way to wait for things to happen? For example for end to end or contract tests? +Definitely! There is an abstraction called `waitForExpect` that will keep +calling your expectations until a timeout or the expectation passes - whatever happens first. + +Please take a look at this example (taken from [`here`](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/end-to-end.js)): + +```javascript +import {render, waitForExpect} from 'react-testing-library' +test('it waits for the data to be loaded', async () => { + const {queryByText, queryByTestId} = render() + + // Initially the loader shows + expect(queryByText('Loading...')).toBeTruthy() + + // This will pass when the state of the component changes once the data is available + // the loader will disappear, and the data will be shown + await waitForExpect(() => expect(queryByText('Loading...')).toBeNull()) + expect(queryByTestId('message').textContent).toMatch(/Hello World/) +}) +``` + +For consistency and making your tests easier to understand, you can use it instead of flushPromises. +
## Other Solutions @@ -634,7 +696,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") | +| [
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") | diff --git a/package.json b/package.json index 77d2e484..e7cc1463 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "keywords": [], "author": "Kent C. Dodds (http://kentcdodds.com/)", "license": "MIT", - "dependencies": {}, + "dependencies": { + "wait-for-expect": "0.4.0" + }, "devDependencies": { "@types/react-dom": "^16.0.4", "axios": "^0.18.0", diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js new file mode 100644 index 00000000..435f4a6e --- /dev/null +++ b/src/__tests__/end-to-end.js @@ -0,0 +1,40 @@ +import React from 'react' +import {render, waitForExpect} from '../' + +const fetchAMessage = () => + new Promise(resolve => { + // we are using random timeout here to simulate a real-time example + // of an async operation calling a callback at a non-deterministic time + const randomTimeout = Math.floor(Math.random() * 100) + setTimeout(() => { + resolve({returnedMessage: 'Hello World'}) + }, randomTimeout) + }) + +class ComponentWithLoader extends React.Component { + state = {loading: true} + async componentDidMount() { + const data = await fetchAMessage() + this.setState({data, loading: false}) // eslint-disable-line + } + render() { + if (this.state.loading) { + return
Loading...
+ } else { + return ( +
+ Loaded this message: {this.state.data.returnedMessage}! +
+ ) + } + } +} + +test('it waits for the data to be loaded', async () => { + const {queryByText, queryByTestId} = render() + + expect(queryByText('Loading...')).toBeTruthy() + + await waitForExpect(() => expect(queryByText('Loading...')).toBeNull()) + expect(queryByTestId('message').textContent).toMatch(/Hello World/) +}) diff --git a/src/index.js b/src/index.js index fd46ac9e..1fd4a9a9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import ReactDOM from 'react-dom' import {Simulate} from 'react-dom/test-utils' +import waitForExpect from 'wait-for-expect' import * as queries from './queries' function render(ui, {container = document.createElement('div')} = {}) { @@ -24,4 +25,4 @@ function flushPromises() { return new Promise(resolve => setImmediate(resolve)) } -export {render, flushPromises, Simulate} +export {render, flushPromises, Simulate, waitForExpect} diff --git a/typings/index.d.ts b/typings/index.d.ts index 55c9963c..153836db 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,4 +1,5 @@ import {Simulate as ReactSimulate} from 'react-dom/test-utils' +import waitForExpect from 'wait-for-expect' interface RenderResult { container: HTMLDivElement @@ -21,3 +22,5 @@ export function render( export function flushPromises(): Promise export const Simulate: typeof ReactSimulate + +export function waitForExpect(): typeof waitForExpect From 69a395f7805415b2983ec9c4cd268b9218a92211 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 29 Mar 2018 09:57:20 -0600 Subject: [PATCH 007/506] fix(waitForExpect): rename to `wait` We avoid a major version bump by continuing to export `waitForExpect` as well. --- README.md | 169 ++++++++++++++---------------------- src/__tests__/deprecated.js | 14 +++ src/__tests__/end-to-end.js | 4 +- src/__tests__/fetch.js | 4 +- src/index.js | 6 +- typings/index.d.ts | 8 ++ 6 files changed, 97 insertions(+), 108 deletions(-) create mode 100644 src/__tests__/deprecated.js diff --git a/README.md b/README.md index 3627c8ac..e89c347b 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,9 @@ facilitate testing implementation details). Read more about this in * [Installation](#installation) * [Usage](#usage) - * [`Simulate`](#simulate) - * [`flushPromises`](#flushpromises) - * [`waitForExpect`](#waitforexpect) * [`render`](#render) + * [`Simulate`](#simulate) + * [`wait`](#wait) * [Custom Jest Matchers](#custom-jest-matchers) * [`toBeInTheDOM`](#tobeinthedom) * [`toHaveTextContent`](#tohavetextcontent) @@ -87,6 +86,8 @@ facilitate testing implementation details). Read more about this in * [`query` APIs](#query-apis) * [Examples](#examples) * [FAQ](#faq) +* [Deprecated APIs](#deprecated-apis) + * [`flushPromises`](#flushpromises) * [Other Solutions](#other-solutions) * [Guiding Principles](#guiding-principles) * [Contributors](#contributors) @@ -110,8 +111,9 @@ This library has a `peerDependencies` listing for `react-dom`. ```javascript // __tests__/fetch.js import React from 'react' -import {render, Simulate, flushPromises} from 'react-testing-library' -import axiosMock from 'axios' +import {render, Simulate, wait} from 'react-testing-library' +import 'react-testing-library/extend-expect' // this adds custom expect matchers +import axiosMock from 'axios' // the mock lives in a __mocks__ directory import Fetch from '../fetch' // see the tests for a full implementation test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => { @@ -128,61 +130,18 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl Simulate.click(getByText('Load Greeting')) // let's wait for our mocked `get` request promise to resolve - await flushPromises() + // wait will wait until the callback doesn't throw an error + await wait(() => getByTestId('greeting-text')) // Assert expect(axiosMock.get).toHaveBeenCalledTimes(1) expect(axiosMock.get).toHaveBeenCalledWith(url) - expect(getByTestId('greeting-text').textContent).toBe('hello there') + expect(getByTestId('greeting-text')).toHaveTextContent('hello there') + // snapshots work great with regular DOM nodes! expect(container.firstChild).toMatchSnapshot() }) ``` -### `Simulate` - -This is simply a re-export from the `Simulate` utility from -`react-dom/test-utils`. See [the docs](https://reactjs.org/docs/test-utils.html#simulate). - -### `flushPromises` - -This is a simple utility that's useful for when your component is doing some -async work that you've mocked out, but you still need to wait until the next -tick of the event loop before you can continue your assertions. It simply -returns a promise that resolves in a `setImmediate`. Especially useful when -you make your test function an `async` function and use -`await flushPromises()`. - -See an example in the section about `render` below. - -### `waitForExpect` - -Defined as: - -```javascript -waitForExpect(expectation: () => void, timeout?: number, interval?: number) => Promise<{}>; -``` - -When in need to wait for non-deterministic periods of time you can use waitForExpect, -to wait for your expectations to pass. Take a look at [`Is there a different way to wait for things to happen?`](#waitForExpect) part of the FAQ, -or the function documentation here: [`wait-for-expect`](https://github.com/TheBrainFamily/wait-for-expect) -or just take a look at this simple example: - -```javascript -... -await waitForExpect(() => expect(queryByLabelText('username')).not.toBeNull()) -getByLabelText('username').value = 'chucknorris' -... -``` - -Another advantage of waitForExpect in comparison to flushPromises, is that -flushPromises will not flush promises that have not been queued up already, -for example, if they will queue up as a result of the initial promises. -In consequence of that, you might have to call flushPromises multiple times to get your components -to your desired state. - -This can happen for example, when you integration test your apollo-connected react components -that go a couple level deep, with queries fired up in consequent components. - ### `render` In the example above, the `render` method returns an object that has a few @@ -283,6 +242,44 @@ const usernameInputElement = getByTestId('username-input') > Learn more about `data-testid`s from the blog post > ["Making your UI tests resilient to change"][data-testid-blog-post] +### `Simulate` + +This is simply a re-export from the `Simulate` utility from +`react-dom/test-utils`. See [the docs](https://reactjs.org/docs/test-utils.html#simulate). + +### `wait` + +Defined as: + +```typescript +function wait( + callback?: () => void, + options?: { + timeout?: number + interval?: number + }, +): Promise +``` + +When in need to wait for non-deterministic periods of time you can use `wait`, +to wait for your expectations to pass. The `wait` function is a small wrapper +around the +[`wait-for-expect`](https://github.com/TheBrainFamily/wait-for-expect) module. +Here's a simple example: + +```javascript +// ... +// wait until the callback does not throw an error. In this case, that means +// it'll wait until we can get a form control with a label that matches "username" +await wait(() => getByLabelText('username')) +getByLabelText('username').value = 'chucknorris' +// ... +``` + +This can be useful when (for example) you integration test your apollo-connected +react components that go a couple level deep, with queries fired up in +consequent components. + ## Custom Jest Matchers There are two simple API which extend the `expect` API of jest for making assertions easier. @@ -600,60 +597,26 @@ react components. -
- -How does flushPromises work and why would I need it? - -As mentioned [before](#flushpromises), `flushPromises` uses -[`setImmediate`][set-immediate] to schedule resolving a promise after any pending -tasks in -[the message queue](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop) -are processed. This includes any promises fired before in your test. +## Deprecated APIs -If there are promise callbacks already in JavaScript's message queue pending to be -processed at the time `flushPromises` is called, then these will be processed before -the promise returned by `flushPromises` is resolved. So when you -`await flushPromises()` the code immediately after it is guaranteed to occur after -all the side effects of your async requests have ocurred. This includes any data -your test components might have requested. - -This is useful for instance, if your components perform any data requests and update -their state with the results when the request is resolved. It's important to note -that this is only effective if you've mocked out your async requests to resolve -immediately (like the `axios` mock we have in the examples). It will not `await` -for promises that are not already resolved by the time you attempt to flush them. - -In case this doesn't work for you the way you would expect, take a look at the -waitForExpect function that should be much more intuitive to use. - -
- -
- -Is there a different way to wait for things to happen? For example for end to end or contract tests? -Definitely! There is an abstraction called `waitForExpect` that will keep -calling your expectations until a timeout or the expectation passes - whatever happens first. - -Please take a look at this example (taken from [`here`](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/end-to-end.js)): - -```javascript -import {render, waitForExpect} from 'react-testing-library' -test('it waits for the data to be loaded', async () => { - const {queryByText, queryByTestId} = render() - - // Initially the loader shows - expect(queryByText('Loading...')).toBeTruthy() - - // This will pass when the state of the component changes once the data is available - // the loader will disappear, and the data will be shown - await waitForExpect(() => expect(queryByText('Loading...')).toBeNull()) - expect(queryByTestId('message').textContent).toMatch(/Hello World/) -}) -``` +### `flushPromises` -For consistency and making your tests easier to understand, you can use it instead of flushPromises. +> This API was deprecated in favor of [`wait`](#wait). We try to avoid having +> two ways to do the same thing and you can accomplish everything with `wait` +> that you could with `flushPromises`. A big advantage of `wait`, is that +> `flushPromises` will not flush promises that have not been queued up already, +> for example, if they will queue up as a result of the initial promises. In +> consequence of that, you might have to call `flushPromises` multiple times to +> get your components to your desired state. You can accomplish the exact same +> behavior with `wait` as you had with `flushPromises` by calling `wait` with +> no arguments: `await wait()` -
+This is a simple utility that's useful for when your component is doing some +async work that you've mocked out, but you still need to wait until the next +tick of the event loop before you can continue your assertions. It simply +returns a promise that resolves in a `setImmediate`. Especially useful when +you make your test function an `async` function and use +`await flushPromises()`. ## Other Solutions diff --git a/src/__tests__/deprecated.js b/src/__tests__/deprecated.js new file mode 100644 index 00000000..33398d43 --- /dev/null +++ b/src/__tests__/deprecated.js @@ -0,0 +1,14 @@ +import {flushPromises, waitForExpect} from '../' + +test('flushPromises (DEPRECATED) still works', async () => { + const fn = jest.fn() + Promise.resolve().then(fn) + await flushPromises() + expect(fn).toHaveBeenCalledTimes(1) +}) + +test('waitForExpect (DEPRECATED) still works', async () => { + const fn = jest.fn() + Promise.resolve().then(fn) + await waitForExpect(() => expect(fn).toHaveBeenCalledTimes(1)) +}) diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js index 435f4a6e..6768dbe7 100644 --- a/src/__tests__/end-to-end.js +++ b/src/__tests__/end-to-end.js @@ -1,5 +1,5 @@ import React from 'react' -import {render, waitForExpect} from '../' +import {render, wait} from '../' const fetchAMessage = () => new Promise(resolve => { @@ -35,6 +35,6 @@ test('it waits for the data to be loaded', async () => { expect(queryByText('Loading...')).toBeTruthy() - await waitForExpect(() => expect(queryByText('Loading...')).toBeNull()) + await wait(() => expect(queryByText('Loading...')).toBeNull()) expect(queryByTestId('message').textContent).toMatch(/Hello World/) }) diff --git a/src/__tests__/fetch.js b/src/__tests__/fetch.js index bddb1489..dac6b330 100644 --- a/src/__tests__/fetch.js +++ b/src/__tests__/fetch.js @@ -1,6 +1,6 @@ import React from 'react' import axiosMock from 'axios' -import {render, Simulate, flushPromises} from '../' +import {render, Simulate, wait} from '../' // instead of importing it, we'll define it inline here // import Fetch from '../fetch' @@ -40,7 +40,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl // Act Simulate.click(getByText('Fetch')) - await flushPromises() + await wait() // Assert expect(axiosMock.get).toHaveBeenCalledTimes(1) diff --git a/src/index.js b/src/index.js index 1fd4a9a9..e0590bf1 100644 --- a/src/index.js +++ b/src/index.js @@ -25,4 +25,8 @@ function flushPromises() { return new Promise(resolve => setImmediate(resolve)) } -export {render, flushPromises, Simulate, waitForExpect} +function wait(callback = () => {}, {timeout, interval} = {}) { + return waitForExpect(callback, timeout, interval) +} + +export {render, flushPromises, Simulate, wait, waitForExpect} diff --git a/typings/index.d.ts b/typings/index.d.ts index 153836db..bdd55a6d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -24,3 +24,11 @@ export function flushPromises(): Promise export const Simulate: typeof ReactSimulate export function waitForExpect(): typeof waitForExpect + +export default function wait( + callback?: () => void, + options?: { + timeout?: number + interval?: number + }, +): Promise From 00832862655691bc4fb6d6720674e509bcdce265 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 29 Mar 2018 10:24:04 -0600 Subject: [PATCH 008/506] fix(wait): be explicit about the timeout and interval --- README.md | 17 ++++++++++++++--- src/index.js | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e89c347b..adda5bd9 100644 --- a/README.md +++ b/README.md @@ -276,9 +276,20 @@ getByLabelText('username').value = 'chucknorris' // ... ``` -This can be useful when (for example) you integration test your apollo-connected -react components that go a couple level deep, with queries fired up in -consequent components. +This can be useful if you have a unit test that mocks API calls and you need +to wait for your mock promises to all resolve. This can also be useful when +(for example) you integration test your apollo-connected react components that +go a couple level deep, with queries fired up in consequent components. + +The default `callback` is a no-op function (used like `await wait()`). This can +be helpful if you only need to wait for one tick of the event loop. + +The default `timeout` is `4500ms` which will keep you under +[Jest's default timeout of `5000ms`](https://facebook.github.io/jest/docs/en/jest-object.html#jestsettimeouttimeout). + +The default `interval` is `50ms`. However it will run your callback immediately +on the next tick of the event loop (in a `setTimeout`) before starting the +intervals. ## Custom Jest Matchers diff --git a/src/index.js b/src/index.js index e0590bf1..d6c27ce2 100644 --- a/src/index.js +++ b/src/index.js @@ -25,7 +25,7 @@ function flushPromises() { return new Promise(resolve => setImmediate(resolve)) } -function wait(callback = () => {}, {timeout, interval} = {}) { +function wait(callback = () => {}, {timeout = 4500, interval = 50} = {}) { return waitForExpect(callback, timeout, interval) } From 66b5a2cf79d91a621095f53f3b8908aed1c9d540 Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Thu, 29 Mar 2018 13:00:50 -0700 Subject: [PATCH 009/506] docs(contributors): add sompylasar (#29) * docs(contributors): add sompylasar * docs(contributors): fix @lgandecki missing docs --- .all-contributorsrc | 13 ++++++++++++- README.md | 6 ++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 5bd7184b..4913b0c7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -104,7 +104,18 @@ "profile": "http://team.thebrain.pro", "contributions": [ "code", - "test" + "test", + "doc" + ] + }, + { + "login": "sompylasar", + "name": "Ivan Babak", + "avatar_url": "https://avatars2.githubusercontent.com/u/498274?v=4", + "profile": "https://sompylasar.github.io", + "contributions": [ + "bug", + "ideas" ] } ] diff --git a/README.md b/README.md index adda5bd9..5e152e39 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-10-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -666,12 +666,10 @@ 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") | - +| [
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") | This project follows the [all-contributors][all-contributors] specification. From e9b603dbbc49eb332449921e5e9de088a7b5d34e Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 29 Mar 2018 17:13:18 -0600 Subject: [PATCH 010/506] feat(getByAltText): add a new query utility --- README.md | 25 ++++++++++++++++--- .../__snapshots__/element-queries.js.snap | 2 ++ src/__tests__/element-queries.js | 20 ++++++++++++--- src/queries.js | 18 +++++++++++++ typings/index.d.ts | 2 ++ 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5e152e39..8a315d15 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,20 @@ const inputNode = getByPlaceholderText('Username') > NOTE: a placeholder is not a good substitute for a label so you should > generally use `getByLabelText` instead. +#### `getByAltText(text: TextMatch): HTMLElement` + +This will return the element (normally an ``) that has the given `alt` +text. Note that it only supports elements which accept an `alt` attribute: +[``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img), +[``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input), +and [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area) +(intentionally excluding [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/applet) as it's deprecated). + +```javascript +// Incredibles 2 Poster +const incrediblesPosterImg = getByText(/incredibles.*poster/) +``` + #### `getByText(text: TextMatch): HTMLElement` This will search for all elements that have a text node with `textContent` @@ -225,9 +239,10 @@ matching the given [`TextMatch`](#textmatch). const aboutAnchorNode = getByText('about') ``` -#### `getByTestId` +#### `getByTestId(text: TextMatch): HTMLElement` -A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. +A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` (and it +also accepts a [`TextMatch`](#textmatch)). ```javascript // @@ -392,10 +407,12 @@ in mind, we recommend this order of priority: method a user finds those elements, so it should be your top preference. 2. `getByPlaceholderText`: [A placeholder is not a substitute for a label](https://www.nngroup.com/articles/form-design-placeholders/). But if that's all you have, then it's better than alternatives. -3. `getByText`: Not useful for forms, but this is the number 1 method a user +3. `getByAltText`: If your element is one which supports `alt` text + (`img`, `area`, and `input`), then you can use this to find that element. +4. `getByText`: Not useful for forms, but this is the number 1 method a user finds other elements (like buttons to click), so it should be your top preference for non-form elements. -4. `getByTestId`: The user cannot see (or hear) these, so this is only +5. `getByTestId`: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by text or it doesn't make sense (the text is dynamic). diff --git a/src/__tests__/__snapshots__/element-queries.js.snap b/src/__tests__/__snapshots__/element-queries.js.snap index ccbe9bc6..6d636275 100644 --- a/src/__tests__/__snapshots__/element-queries.js.snap +++ b/src/__tests__/__snapshots__/element-queries.js.snap @@ -8,6 +8,8 @@ exports[`get throws a useful error message 3`] = `"Unable to find an element wit exports[`get throws a useful error message 4`] = `"Unable to find an element by: [data-testid=\\"LucyRicardo\\"]"`; +exports[`get throws a useful error message 5`] = `"Unable to find an element with the alt text: LucyRicardo"`; + exports[`label with no form control 1`] = `"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`; exports[`totally empty label 1`] = `"Found a label with the text of: , however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`; diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index a5912c8d..7c4d09d5 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -8,23 +8,30 @@ test('query can return null', () => { queryByPlaceholderText, queryByText, queryByTestId, + queryByAltText, } = render(
) expect(queryByTestId('LucyRicardo')).toBeNull() expect(queryByLabelText('LucyRicardo')).toBeNull() expect(queryByPlaceholderText('LucyRicardo')).toBeNull() expect(queryByText('LucyRicardo')).toBeNull() + expect(queryByAltText('LucyRicardo')).toBeNull() }) test('get throws a useful error message', () => { - const {getByLabelText, getByPlaceholderText, getByText, getByTestId} = render( -
, - ) + const { + getByLabelText, + getByPlaceholderText, + getByText, + getByTestId, + getByAltText, + } = render(
) expect(() => getByLabelText('LucyRicardo')).toThrowErrorMatchingSnapshot() expect(() => getByPlaceholderText('LucyRicardo'), ).toThrowErrorMatchingSnapshot() expect(() => getByText('LucyRicardo')).toThrowErrorMatchingSnapshot() expect(() => getByTestId('LucyRicardo')).toThrowErrorMatchingSnapshot() + expect(() => getByAltText('LucyRicardo')).toThrowErrorMatchingSnapshot() }) test('get can get form controls by label text', () => { @@ -67,6 +74,13 @@ test('totally empty label', () => { expect(() => getByLabelText('')).toThrowErrorMatchingSnapshot() }) +test('get element by its alt text', () => { + const {getByAltText} = render( + finding nemo poster, + ) + expect(getByAltText(/fin.*nem.*poster$/i).src).toBe('/finding-nemo.png') +}) + test('using jest helpers to assert element states', () => { const {queryByTestId} = render(2) diff --git a/src/queries.js b/src/queries.js index 485e2826..0f218add 100644 --- a/src/queries.js +++ b/src/queries.js @@ -121,6 +121,22 @@ function getByText(container, text, ...rest) { return el } +function queryByAltText(container, alt) { + return ( + Array.from(container.querySelectorAll('img,input,area')).find(node => + matches(node.getAttribute('alt'), node, alt), + ) || null + ) +} + +function getByAltText(container, alt) { + const el = queryByAltText(container, alt) + if (!el) { + throw new Error(`Unable to find an element with the alt text: ${alt}`) + } + return el +} + export { queryByPlaceholderText, getByPlaceholderText, @@ -128,6 +144,8 @@ export { getByText, queryByLabelText, getByLabelText, + queryByAltText, + getByAltText, queryByTestId, getByTestId, } diff --git a/typings/index.d.ts b/typings/index.d.ts index bdd55a6d..f9b3c606 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -12,6 +12,8 @@ interface RenderResult { getByPlaceholderText: (id: string) => HTMLElement queryByLabelText: (id: string) => HTMLElement | null getByLabelText: (id: string) => HTMLElement + queryByAltText: (text: string) => HTMLElement | null + getByAltText: (text: string) => HTMLElement } export function render( From 979256835c49fdaf2576d116fd4a530eac8f7922 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 29 Mar 2018 17:29:48 -0600 Subject: [PATCH 011/506] fix(textMatch): if the textToMatch is not a string then it wont match --- src/__tests__/element-queries.js | 5 ++++- src/utils.js | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 7c4d09d5..4161f3fe 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -76,7 +76,10 @@ test('totally empty label', () => { test('get element by its alt text', () => { const {getByAltText} = render( - finding nemo poster, +
+ + finding nemo poster +
, ) expect(getByAltText(/fin.*nem.*poster$/i).src).toBe('/finding-nemo.png') }) diff --git a/src/utils.js b/src/utils.js index 87ecef8b..b37542f1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,8 @@ //eslint-disable-next-line import/prefer-default-export export function matches(textToMatch, node, matcher) { + if (typeof textToMatch !== 'string') { + return false + } if (typeof matcher === 'string') { return textToMatch.toLowerCase().includes(matcher.toLowerCase()) } else if (typeof matcher === 'function') { From 6f01eebcb79ecb23afecd1645af2d05300c1f7e3 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 29 Mar 2018 18:19:02 -0600 Subject: [PATCH 012/506] docs: update getByAltText docs Also reorder things a bit. Sorta different categories, but makes sense to me. --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8a315d15..e9aa6f9a 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,16 @@ const inputNode = getByPlaceholderText('Username') > NOTE: a placeholder is not a good substitute for a label so you should > generally use `getByLabelText` instead. +#### `getByText(text: TextMatch): HTMLElement` + +This will search for all elements that have a text node with `textContent` +matching the given [`TextMatch`](#textmatch). + +```javascript +// About โ„น๏ธ +const aboutAnchorNode = getByText('about') +``` + #### `getByAltText(text: TextMatch): HTMLElement` This will return the element (normally an ``) that has the given `alt` @@ -226,17 +236,7 @@ and [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area) ```javascript // Incredibles 2 Poster -const incrediblesPosterImg = getByText(/incredibles.*poster/) -``` - -#### `getByText(text: TextMatch): HTMLElement` - -This will search for all elements that have a text node with `textContent` -matching the given [`TextMatch`](#textmatch). - -```javascript -// About โ„น๏ธ -const aboutAnchorNode = getByText('about') +const incrediblesPosterImg = getByAltText(/incredibles.*poster$/i) ``` #### `getByTestId(text: TextMatch): HTMLElement` @@ -407,11 +407,11 @@ in mind, we recommend this order of priority: method a user finds those elements, so it should be your top preference. 2. `getByPlaceholderText`: [A placeholder is not a substitute for a label](https://www.nngroup.com/articles/form-design-placeholders/). But if that's all you have, then it's better than alternatives. -3. `getByAltText`: If your element is one which supports `alt` text - (`img`, `area`, and `input`), then you can use this to find that element. -4. `getByText`: Not useful for forms, but this is the number 1 method a user +3. `getByText`: Not useful for forms, but this is the number 1 method a user finds other elements (like buttons to click), so it should be your top preference for non-form elements. +4. `getByAltText`: If your element is one which supports `alt` text + (`img`, `area`, and `input`), then you can use this to find that element. 5. `getByTestId`: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by text or it doesn't make sense (the text is dynamic). From fd2df8d18652786a95bce34741180137f9d2cef2 Mon Sep 17 00:00:00 2001 From: ants Date: Fri, 30 Mar 2018 10:11:13 +0530 Subject: [PATCH 013/506] fix(jest-matchers): message should be function and added test cases for the coverage as well (#32) --- src/__tests__/element-queries.js | 14 ++++++++++++++ src/jest-extensions.js | 30 ++++++++++++++++-------------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 4161f3fe..43326413 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -95,6 +95,20 @@ test('using jest helpers to assert element states', () => { expect(() => expect(queryByTestId('count-value2')).toHaveTextContent('2'), ).toThrowError() + + // negative test cases wrapped in throwError assertions for coverage. + expect(() => + expect(queryByTestId('count-value')).not.toBeInTheDOM(), + ).toThrowError() + expect(() => + expect(queryByTestId('count-value1')).toBeInTheDOM(), + ).toThrowError() + expect(() => + expect(queryByTestId('count-value')).toHaveTextContent('3'), + ).toThrowError() + expect(() => + expect(queryByTestId('count-value')).not.toHaveTextContent('2'), + ).toThrowError() }) /* eslint jsx-a11y/label-has-for:0 */ diff --git a/src/jest-extensions.js b/src/jest-extensions.js index 69e1e92c..c94e29f6 100644 --- a/src/jest-extensions.js +++ b/src/jest-extensions.js @@ -18,7 +18,7 @@ const extensions = { getDisplayName(received) if (received) { return { - message: + message: () => `${matcherHint( '.not.toBeInTheDOM', 'received', @@ -29,7 +29,7 @@ const extensions = { } } else { return { - message: + message: () => `${matcherHint( '.toBeInTheDOM', 'received', @@ -53,22 +53,24 @@ const extensions = { const pass = matches(textContent, htmlElement, checkWith) if (pass) { return { - message: assertMessage( - '.not.toHaveTextContent', - 'Expected value not equals to', - htmlElement, - checkWith, - ), + message: () => + assertMessage( + '.not.toHaveTextContent', + 'Expected value not equals to', + htmlElement, + checkWith, + ), pass: true, } } else { return { - message: assertMessage( - '.toHaveTextContent', - 'Expected value equals to', - htmlElement, - checkWith, - ), + message: () => + assertMessage( + '.toHaveTextContent', + 'Expected value equals to', + htmlElement, + checkWith, + ), pass: false, } } From a8857b633f0903b3860ac2589da553d4a8a6810d Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 2 Apr 2018 07:42:28 -0600 Subject: [PATCH 014/506] fix(extend-expect): remove expect import Closes #34 --- src/extend-expect.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/extend-expect.js b/src/extend-expect.js index 58359822..b2b99844 100644 --- a/src/extend-expect.js +++ b/src/extend-expect.js @@ -1,7 +1,4 @@ -import expect from 'expect' //eslint-disable-line import/no-extraneous-dependencies import extensions from './jest-extensions' const {toBeInTheDOM, toHaveTextContent} = extensions expect.extend({toBeInTheDOM, toHaveTextContent}) - -export default expect From 2bf760c6d5163907a2a59c2e876b360f5cde046f Mon Sep 17 00:00:00 2001 From: Jesse Day Date: Mon, 2 Apr 2018 08:01:48 -0700 Subject: [PATCH 015/506] fix(definitions): export correct types for wait and waitForExpect (#33) * chore(contributors): add myself * fix(definitions): export correct types for wait and waitForExpect --- .all-contributorsrc | 9 +++++++++ README.md | 6 ++++-- typings/index.d.ts | 6 ++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4913b0c7..05f6e72f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -117,6 +117,15 @@ "bug", "ideas" ] + }, + { + "login": "jday3", + "name": "Jesse Day", + "avatar_url": "https://avatars3.githubusercontent.com/u/4439618?v=4", + "profile": "https://github.com/jday3", + "contributions": [ + "code" + ] } ] } diff --git a/README.md b/README.md index e9aa6f9a..1fd7de3e 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-11-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -683,10 +683,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") | +| [
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/typings/index.d.ts b/typings/index.d.ts index f9b3c606..07c15511 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,5 @@ import {Simulate as ReactSimulate} from 'react-dom/test-utils' -import waitForExpect from 'wait-for-expect' +export {default as waitForExpect} from 'wait-for-expect' interface RenderResult { container: HTMLDivElement @@ -25,9 +25,7 @@ export function flushPromises(): Promise export const Simulate: typeof ReactSimulate -export function waitForExpect(): typeof waitForExpect - -export default function wait( +export function wait( callback?: () => void, options?: { timeout?: number From 3a5eab870388611376736d1de97b7d21b52296c1 Mon Sep 17 00:00:00 2001 From: tsiq-swyx <35976578+tsiq-swyx@users.noreply.github.com> Date: Mon, 2 Apr 2018 15:19:23 -0400 Subject: [PATCH 016/506] docs: add documentation for using Matchers w/ Typescript (#37) as mentioned in #36 --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 1fd7de3e..45790f52 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ 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) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Examples](#examples) @@ -343,6 +344,34 @@ expect(getByTestId('count-value')).toHaveTextContent('2') expect(getByTestId('count-value')).not.toHaveTextContent('21') // ... ``` +### 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: + +```typescript +// this adds custom expect matchers +import 'react-testing-library/extend-expect'; +interface ExtendedMatchers extends jest.Matchers { + toHaveTextContent: (htmlElement: string) => object; + toBeInTheDOM: () => void; +} +test('renders the tooltip as expected', async () => { + const { + // getByLabelText, + getByText, + // getByTestId, + container + } = render(Child); + // tests rendering of the child + getByText('Child'); + // tests rendering of tooltip label + (expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent( + 'hello world' + ); + // snapshots work great with regular DOM nodes! + expect(container.firstChild).toMatchSnapshot(); +}); +``` ## `TextMatch` From 23d6242c73c343a797c6966bda991c7cd5a5fa5c Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 2 Apr 2018 16:51:16 -0600 Subject: [PATCH 017/506] fix(release): manually release a patch version There was an issue with a patch release, so this manual-releases.md change is to release a new patch version. Reference: #33 --- other/manual-releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/other/manual-releases.md b/other/manual-releases.md index f4c5b113..2834084e 100644 --- a/other/manual-releases.md +++ b/other/manual-releases.md @@ -40,4 +40,4 @@ change is to release a new patch version. Reference: # ``` -The number of times we've had to do a manual release is: 1 +The number of times we've had to do a manual release is: 2 From 825cd486e848e52533d0794666b440f02a30e709 Mon Sep 17 00:00:00 2001 From: Bruno Dias Date: Tue, 3 Apr 2018 00:19:39 -0300 Subject: [PATCH 018/506] docs: update README.md (#39) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45790f52..8d67bcde 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ allows your tests to give you more confidence that your application will work when a real user uses it. This library is a replacement for [enzyme](http://airbnb.io/enzyme/). While you -_can_ follow these guidlines using enzyme itself, enforcing this is harder +_can_ follow these guidelines using enzyme itself, enforcing this is harder because of all the extra utilities that enzyme provides (utilities which facilitate testing implementation details). Read more about this in [the FAQ](#faq) below. From 34c974d8090b29096382e4e57784897192c88160 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Wed, 4 Apr 2018 07:59:12 -0600 Subject: [PATCH 019/506] fix(label): support aria-label (#42) --- README.md | 5 +++-- src/__tests__/element-queries.js | 6 ++++++ src/queries.js | 25 +++++++++++-------------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8d67bcde..5d25e52b 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,9 @@ const inputNode = getByLabelText('Username') // For this case, you can provide a `selector` in the options: const inputNode = getByLabelText('username', {selector: 'input'}) // and that would work +// Note that will also work, but take +// care because this is not a label that users can see on the page. So +// the purpose of your input should be obvious for those users. ``` > Note: This method will throw an error if it cannot find the node. If you don't @@ -712,12 +715,10 @@ 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/__tests__/element-queries.js b/src/__tests__/element-queries.js index 43326413..f8fd4830 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -74,6 +74,12 @@ test('totally empty label', () => { expect(() => getByLabelText('')).toThrowErrorMatchingSnapshot() }) +test('getByLabelText with aria-label', () => { + // not recommended normally, but supported for completeness + const {queryByLabelText} = render() + expect(queryByLabelText('bat')).toBeInTheDOM() +}) + test('get element by its alt text', () => { const {getByAltText} = render(
diff --git a/src/queries.js b/src/queries.js index 0f218add..237f39e9 100644 --- a/src/queries.js +++ b/src/queries.js @@ -15,7 +15,7 @@ function queryLabelByText(container, text) { function queryByLabelText(container, text, {selector = '*'} = {}) { const label = queryLabelByText(container, text) if (!label) { - return null + return queryByAttribute('aria-label', container, text) } /* istanbul ignore if */ if (label.control) { @@ -46,22 +46,21 @@ function queryByText(container, text, {selector = '*'} = {}) { ) } -function queryByPlaceholderText(container, text) { +// this is just a utility and not an exposed query. +// There are no plans to expose this. +function queryByAttribute(attribute, container, text) { return ( - Array.from(container.querySelectorAll('[placeholder]')).find(node => - matches(node.getAttribute('placeholder'), node, text), + Array.from(container.querySelectorAll(`[${attribute}]`)).find(node => + matches(node.getAttribute(attribute), node, text), ) || null ) } -function queryByTestId(container, id) { - return container.querySelector(getDataTestIdSelector(id)) -} - -function getDataTestIdSelector(id) { - return `[data-testid="${id}"]` -} +const queryByPlaceholderText = queryByAttribute.bind(null, 'placeholder') +const queryByTestId = queryByAttribute.bind(null, 'data-testid') +// this is just a utility and not an exposed query. +// There are no plans to expose this. function getText(node) { return Array.from(node.childNodes) .filter( @@ -79,9 +78,7 @@ function getText(node) { function getByTestId(container, id, ...rest) { const el = queryByTestId(container, id, ...rest) if (!el) { - throw new Error( - `Unable to find an element by: ${getDataTestIdSelector(id)}`, - ) + throw new Error(`Unable to find an element by: [data-testid="${id}"]`) } return el } From 7267acd9743b328b3bc7630dc899ad28846071ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 5 Apr 2018 18:08:21 -0300 Subject: [PATCH 020/506] feat(matchers): add toHaveAttribute custom matcher (closes #43) (#44) * Add toHaveAttribute custom matcher * Add gnapse as contributor * Generate messages more consistent with jest's other matchers * Add comment to matcher hint * Add tests to cover toHaveAttribute custom matcher --- .all-contributorsrc | 11 +++++ README.md | 56 +++++++++++++++++------- src/__tests__/element-queries.js | 30 +++++++++++++ src/extend-expect.js | 4 +- src/jest-extensions.js | 73 ++++++++++++++++++++++++++++---- 5 files changed, 149 insertions(+), 25 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 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 From 6d9c368d921a66a1ed12c2a05372d776e6f236db Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 6 Apr 2018 07:36:53 -0600 Subject: [PATCH 021/506] feat: extract most of the logic to dom-testing-library BREAKING CHANGE: This removes some deprecated APIs, but should be a safe upgrade for you if you're not using `flushPromises` ( use `wait` instead) --- .github/ISSUE_TEMPLATE.md | 5 + README.md | 126 ++------------- extend-expect.js | 1 - package.json | 17 +- .../__snapshots__/element-queries.js.snap | 15 -- src/__tests__/deprecated.js | 14 -- src/__tests__/element-queries.js | 150 ------------------ src/__tests__/text-matchers.js | 25 --- src/extend-expect.js | 4 - src/index.js | 15 +- src/jest-extensions.js | 137 ---------------- src/queries.js | 150 ------------------ src/utils.js | 13 -- typings/index.d.ts | 3 - 14 files changed, 30 insertions(+), 645 deletions(-) delete mode 100644 extend-expect.js delete mode 100644 src/__tests__/__snapshots__/element-queries.js.snap delete mode 100644 src/__tests__/deprecated.js delete mode 100644 src/__tests__/element-queries.js delete mode 100644 src/__tests__/text-matchers.js delete mode 100644 src/extend-expect.js delete mode 100644 src/jest-extensions.js delete mode 100644 src/queries.js delete mode 100644 src/utils.js diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6a7b4bff..115d96e0 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,9 @@ @@ -79,17 +83,10 @@ facilitate testing implementation details). Read more about this in * [`render`](#render) * [`Simulate`](#simulate) * [`wait`](#wait) -* [Custom Jest Matchers](#custom-jest-matchers) - * [`toBeInTheDOM`](#tobeinthedom) - * [`toHaveTextContent`](#tohavetextcontent) - * [`toHaveAttribute`](#tohaveattribute) - * [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Examples](#examples) * [FAQ](#faq) -* [Deprecated APIs](#deprecated-apis) - * [`flushPromises`](#flushpromises) * [Other Solutions](#other-solutions) * [Guiding Principles](#guiding-principles) * [Contributors](#contributors) @@ -108,13 +105,17 @@ npm install --save-dev react-testing-library This library has a `peerDependencies` listing for `react-dom`. +You may also be interested in installing `dom-testing-library` so you can use +[the custom jest matchers](https://github.com/kentcdodds/dom-testing-library/blob/master/README.md#custom-jest-matchers) + ## Usage ```javascript // __tests__/fetch.js import React from 'react' import {render, Simulate, wait} from 'react-testing-library' -import 'react-testing-library/extend-expect' // this adds custom expect matchers +// this add custom expect matchers from dom-testing-library +import 'dom-testing-library/extend-expect' import axiosMock from 'axios' // the mock lives in a __mocks__ directory import Fetch from '../fetch' // see the tests for a full implementation @@ -312,94 +313,6 @@ The default `interval` is `50ms`. However it will run your callback immediately on the next tick of the event loop (in a `setTimeout`) before starting the intervals. -## Custom Jest Matchers - -There are two simple API which extend the `expect` API of jest for making assertions easier. - -### `toBeInTheDOM` - -This allows you to assert whether an element present in the DOM or not. - -```javascript -// add the custom expect matchers -import 'react-testing-library/extend-expect' - -// ... -const {queryByTestId} = render(2) -expect(queryByTestId('count-value')).toBeInTheDOM() -expect(queryByTestId('count-value1')).not.toBeInTheDOM() -// ... -``` - -> Note: when using `toBeInTheDOM`, make sure you use a query function -> (like `queryByTestId`) rather than a get function (like `getByTestId`). -> Otherwise the `get*` function could throw an error before your assertion. - -### `toHaveTextContent` - -This API allows you to check whether the given element has a text content or not. - -```javascript -// add the custom expect matchers -import 'react-testing-library/extend-expect' - -// ... -const {getByTestId} = render(2) -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: - -```typescript -// this adds custom expect matchers -import 'react-testing-library/extend-expect' -interface ExtendedMatchers extends jest.Matchers { - toHaveTextContent: (htmlElement: string) => object - toBeInTheDOM: () => void -} -test('renders the tooltip as expected', async () => { - const { - // getByLabelText, - getByText, - // getByTestId, - container, - } = render(Child) - // tests rendering of the child - getByText('Child') - // tests rendering of tooltip label - ;(expect(getByText('hello world')) as ExtendedMatchers).toHaveTextContent( - 'hello world', - ) - // snapshots work great with regular DOM nodes! - expect(container.firstChild).toMatchSnapshot() -}) -``` - ## `TextMatch` Several APIs accept a `TextMatch` which can be a `string`, `regex` or a @@ -681,27 +594,6 @@ react components. -## Deprecated APIs - -### `flushPromises` - -> This API was deprecated in favor of [`wait`](#wait). We try to avoid having -> two ways to do the same thing and you can accomplish everything with `wait` -> that you could with `flushPromises`. A big advantage of `wait`, is that -> `flushPromises` will not flush promises that have not been queued up already, -> for example, if they will queue up as a result of the initial promises. In -> consequence of that, you might have to call `flushPromises` multiple times to -> get your components to your desired state. You can accomplish the exact same -> behavior with `wait` as you had with `flushPromises` by calling `wait` with -> no arguments: `await wait()` - -This is a simple utility that's useful for when your component is doing some -async work that you've mocked out, but you still need to wait until the next -tick of the event loop before you can continue your assertions. It simply -returns a promise that resolves in a `setImmediate`. Especially useful when -you make your test function an `async` function and use -`await flushPromises()`. - ## Other Solutions In preparing this project, diff --git a/extend-expect.js b/extend-expect.js deleted file mode 100644 index 3cee4049..00000000 --- a/extend-expect.js +++ /dev/null @@ -1 +0,0 @@ -require('./dist/extend-expect') diff --git a/package.json b/package.json index e7cc1463..f506f47a 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,24 @@ }, "files": [ "dist", - "typings", - "extend-expect.js" + "typings" + ], + "keywords": [ + "testing", + "react", + "ui", + "dom", + "jsdom", + "unit", + "integration", + "functional", + "end-to-end", + "e2e" ], - "keywords": [], "author": "Kent C. Dodds (http://kentcdodds.com/)", "license": "MIT", "dependencies": { + "dom-testing-library": "^1.0.0", "wait-for-expect": "0.4.0" }, "devDependencies": { diff --git a/src/__tests__/__snapshots__/element-queries.js.snap b/src/__tests__/__snapshots__/element-queries.js.snap deleted file mode 100644 index 6d636275..00000000 --- a/src/__tests__/__snapshots__/element-queries.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`get throws a useful error message 1`] = `"Unable to find a label with the text of: LucyRicardo"`; - -exports[`get throws a useful error message 2`] = `"Unable to find an element with the placeholder text of: LucyRicardo"`; - -exports[`get throws a useful error message 3`] = `"Unable to find an element with the text: LucyRicardo. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible."`; - -exports[`get throws a useful error message 4`] = `"Unable to find an element by: [data-testid=\\"LucyRicardo\\"]"`; - -exports[`get throws a useful error message 5`] = `"Unable to find an element with the alt text: LucyRicardo"`; - -exports[`label with no form control 1`] = `"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`; - -exports[`totally empty label 1`] = `"Found a label with the text of: , however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`; diff --git a/src/__tests__/deprecated.js b/src/__tests__/deprecated.js deleted file mode 100644 index 33398d43..00000000 --- a/src/__tests__/deprecated.js +++ /dev/null @@ -1,14 +0,0 @@ -import {flushPromises, waitForExpect} from '../' - -test('flushPromises (DEPRECATED) still works', async () => { - const fn = jest.fn() - Promise.resolve().then(fn) - await flushPromises() - expect(fn).toHaveBeenCalledTimes(1) -}) - -test('waitForExpect (DEPRECATED) still works', async () => { - const fn = jest.fn() - Promise.resolve().then(fn) - await waitForExpect(() => expect(fn).toHaveBeenCalledTimes(1)) -}) diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js deleted file mode 100644 index cf21ecd0..00000000 --- a/src/__tests__/element-queries.js +++ /dev/null @@ -1,150 +0,0 @@ -import React from 'react' -import {render} from '../' -import '../extend-expect' - -test('query can return null', () => { - const { - queryByLabelText, - queryByPlaceholderText, - queryByText, - queryByTestId, - queryByAltText, - } = render(
) - expect(queryByTestId('LucyRicardo')).toBeNull() - expect(queryByLabelText('LucyRicardo')).toBeNull() - expect(queryByPlaceholderText('LucyRicardo')).toBeNull() - expect(queryByText('LucyRicardo')).toBeNull() - expect(queryByAltText('LucyRicardo')).toBeNull() -}) - -test('get throws a useful error message', () => { - const { - getByLabelText, - getByPlaceholderText, - getByText, - getByTestId, - getByAltText, - } = render(
) - expect(() => getByLabelText('LucyRicardo')).toThrowErrorMatchingSnapshot() - expect(() => - getByPlaceholderText('LucyRicardo'), - ).toThrowErrorMatchingSnapshot() - expect(() => getByText('LucyRicardo')).toThrowErrorMatchingSnapshot() - expect(() => getByTestId('LucyRicardo')).toThrowErrorMatchingSnapshot() - expect(() => getByAltText('LucyRicardo')).toThrowErrorMatchingSnapshot() -}) - -test('get can get form controls by label text', () => { - const {getByLabelText} = render( -
- -
- - -
-
- - -
-
, - ) - expect(getByLabelText('1st').id).toBe('first-id') - expect(getByLabelText('2nd').id).toBe('second-id') - expect(getByLabelText('3rd').id).toBe('third-id') -}) - -test('get can get form controls by placeholder', () => { - const {getByPlaceholderText} = render( - , - ) - expect(getByPlaceholderText('username').id).toBe('username-id') -}) - -test('label with no form control', () => { - const {getByLabelText, queryByLabelText} = render() - expect(queryByLabelText('alone')).toBeNull() - expect(() => getByLabelText('alone')).toThrowErrorMatchingSnapshot() -}) - -test('totally empty label', () => { - const {getByLabelText, queryByLabelText} = render(