diff --git a/.all-contributorsrc b/.all-contributorsrc index 5bde84c2..90dd9791 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -55,6 +55,18 @@ "contributions": [ "doc" ] + }, + { + "login": "pbomb", + "name": "Matt Parrish", + "avatar_url": "https://avatars0.githubusercontent.com/u/1402095?v=4", + "profile": "https://github.com/pbomb", + "contributions": [ + "bug", + "code", + "doc", + "test" + ] } ] } diff --git a/README.md b/README.md index 1fe420d9..bc9d6383 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-5-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -86,10 +86,10 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl }), ) const url = '/greeting' - const {queryByTestId, container} = render() + const {getByTestId, container} = render() // Act - Simulate.click(queryByTestId('load-greeting')) + Simulate.click(getByTestId('load-greeting')) // let's wait for our mocked `get` request promise to resolve await flushPromises() @@ -97,7 +97,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl // Assert expect(axiosMock.get).toHaveBeenCalledTimes(1) expect(axiosMock.get).toHaveBeenCalledWith(url) - expect(queryByTestId('greeting-text').textContent).toBe('hello there') + expect(getByTestId('greeting-text').textContent).toBe('hello there') expect(container.firstChild).toMatchSnapshot() }) ``` @@ -146,18 +146,34 @@ unmount() // your component has been unmounted and now: container.innerHTML === '' ``` +#### `getByTestId` + +A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` except +that it will throw an Error if no matching element is found. Read more about +`data-testid`s below. + +```javascript +const usernameInputElement = getByTestId('username-input') +usernameInputElement.value = 'new value' +Simulate.change(usernameInputElement) +``` + #### `queryByTestId` -A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. Read -more about `data-testid`s below. +A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` +(Note: just like `querySelector`, this could return null if no matching element +is found, which may lead to harder-to-understand error messages). Read more about +`data-testid`s below. ```javascript -const usernameInputElement = queryByTestId('username-input') +// assert something doesn't exist +// (you couldn't do this with `getByTestId`) +expect(queryByTestId('username-input')).toBeNull() ``` ## More on `data-testid`s -The `queryByTestId` utility is referring to the practice of using `data-testid` +The `getByTestId` and `queryByTestId` utilities refer to the practice of using `data-testid` attributes to identify individual elements in your rendered component. This is one of the practices this library is intended to encourage. @@ -186,14 +202,14 @@ prefer to update the props of a rendered component in your test, the easiest way to do that is: ```javascript -const {container, queryByTestId} = render() -expect(queryByTestId('number-display').textContent).toBe('1') +const {container, getByTestId} = render() +expect(getByTestId('number-display').textContent).toBe('1') // re-render the same component with different props // but pass the same container in the options argument. // which will cause a re-render of the same instance (normal React behavior). render(, {container}) -expect(queryByTestId('number-display').textContent).toBe('2') +expect(getByTestId('number-display').textContent).toBe('2') ``` [Open the tests](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/number-display.js) @@ -219,14 +235,16 @@ jest.mock('react-transition-group', () => { }) test('you can mock things with jest.mock', () => { - const {queryByTestId} = render() + const {getByTestId, queryByTestId} = render( + , + ) expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists // hide the message - Simulate.click(queryByTestId('toggle-message')) + Simulate.click(getByTestId('toggle-message')) // in the real world, the CSSTransition component would take some time // before finishing the animation which would actually hide the message. // So we've mocked it out for our tests to make it happen instantly - expect(queryByTestId('hidden-message')).toBeFalsy() // we just care it doesn't exist + expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist }) ``` @@ -247,6 +265,14 @@ something more Learn more about how Jest mocks work from my blog post: ["But really, what is a JavaScript mock?"](https://blog.kentcdodds.com/but-really-what-is-a-javascript-mock-10d060966f7d) +**What if I want to verify that an element does NOT exist?** + +You typically will get access to rendered elements using the `getByTestId` utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the `queryByTestId` utility which will return the element if found or `null` if not. + +```javascript +expect(queryByTestId('thing-that-does-not-exist')).toBeNull() +``` + **I don't want to use `data-testid` attributes for everything. Do I have to?** Definitely not. That said, a common reason people don't like the `data-testid` @@ -286,18 +312,18 @@ Or you could include the index or an ID in your attribute:
  • {item.text}
  • ``` -And then you could use the `queryByTestId`: +And then you could use the `getByTestId` utility: ```javascript const items = [ /* your items */ ] -const {queryByTestId} = render(/* your component with the items */) -const thirdItem = queryByTestId(`item-${items[2].id}`) +const {getByTestId} = render(/* your component with the items */) +const thirdItem = getByTestId(`item-${items[2].id}`) ``` **What about enzyme is "bloated with complexity and features" and "encourage poor testing -practices"** +practices"?** Most of the damaging features have to do with encouraging testing implementation details. Primarily, these are @@ -358,8 +384,8 @@ 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") | -| :---: | :---: | :---: | :---: | :---: | +| [
    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") | +| :---: | :---: | :---: | :---: | :---: | :---: | diff --git a/package.json b/package.json index e662a13a..8794fdfe 100644 --- a/package.json +++ b/package.json @@ -61,4 +61,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__/__snapshots__/element-queries.js.snap b/src/__tests__/__snapshots__/element-queries.js.snap new file mode 100644 index 00000000..722e413e --- /dev/null +++ b/src/__tests__/__snapshots__/element-queries.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getByTestId finds matching element 1`] = ` + +`; + +exports[`getByTestId throws error when no matching element exists 1`] = `"Unable to find element by [data-testid=\\"unknown-data-testid\\"]"`; + +exports[`queryByTestId finds matching element 1`] = ` + +`; diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js new file mode 100644 index 00000000..270865fe --- /dev/null +++ b/src/__tests__/element-queries.js @@ -0,0 +1,26 @@ +import React from 'react' +import {render} from '../' + +const TestComponent = () => + +test('queryByTestId finds matching element', () => { + const {queryByTestId} = render() + expect(queryByTestId('test-component')).toMatchSnapshot() +}) + +test('queryByTestId returns null when no matching element exists', () => { + const {queryByTestId} = render() + expect(queryByTestId('unknown-data-testid')).toBeNull() +}) + +test('getByTestId finds matching element', () => { + const {getByTestId} = render() + expect(getByTestId('test-component')).toMatchSnapshot() +}) + +test('getByTestId throws error when no matching element exists', () => { + const {getByTestId} = render() + expect(() => + getByTestId('unknown-data-testid'), + ).toThrowErrorMatchingSnapshot() +}) diff --git a/src/__tests__/fetch.js b/src/__tests__/fetch.js index 6b2c5f01..6cca6f44 100644 --- a/src/__tests__/fetch.js +++ b/src/__tests__/fetch.js @@ -37,16 +37,16 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl }), ) const url = '/greeting' - const {queryByTestId, container} = render() + const {getByTestId, container} = render() // Act - Simulate.click(queryByTestId('load-greeting')) + Simulate.click(getByTestId('load-greeting')) await flushPromises() // Assert expect(axiosMock.get).toHaveBeenCalledTimes(1) expect(axiosMock.get).toHaveBeenCalledWith(url) - expect(queryByTestId('greeting-text').textContent).toBe('hello there') + expect(getByTestId('greeting-text').textContent).toBe('hello there') expect(container.firstChild).toMatchSnapshot() }) diff --git a/src/__tests__/mock.react-transition-group.js b/src/__tests__/mock.react-transition-group.js index 76d0b0ba..b46cf4b3 100644 --- a/src/__tests__/mock.react-transition-group.js +++ b/src/__tests__/mock.react-transition-group.js @@ -39,10 +39,12 @@ jest.mock('react-transition-group', () => { }) test('you can mock things with jest.mock', () => { - const {queryByTestId} = render() + const {getByTestId, queryByTestId} = render( + , + ) expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists // hide the message - Simulate.click(queryByTestId('toggle-message')) + Simulate.click(getByTestId('toggle-message')) // in the real world, the CSSTransition component would take some time // before finishing the animation which would actually hide the message. // So we've mocked it out for our tests to make it happen instantly diff --git a/src/__tests__/number-display.js b/src/__tests__/number-display.js index f54843fd..60a9ea32 100644 --- a/src/__tests__/number-display.js +++ b/src/__tests__/number-display.js @@ -16,14 +16,14 @@ class NumberDisplay extends React.Component { } test('calling render with the same component on the same container does not remount', () => { - const {container, queryByTestId} = render() - expect(queryByTestId('number-display').textContent).toBe('1') + const {container, getByTestId} = render() + expect(getByTestId('number-display').textContent).toBe('1') // re-render the same component with different props // but pass the same container in the options argument. // which will cause a re-render of the same instance (normal React behavior). render(, {container}) - expect(queryByTestId('number-display').textContent).toBe('2') + expect(getByTestId('number-display').textContent).toBe('2') - expect(queryByTestId('instance-id').textContent).toBe('1') + expect(getByTestId('instance-id').textContent).toBe('1') }) diff --git a/src/__tests__/react-redux.js b/src/__tests__/react-redux.js index eb9f527e..c811c515 100644 --- a/src/__tests__/react-redux.js +++ b/src/__tests__/react-redux.js @@ -82,27 +82,27 @@ function renderWithRedux( } test('can render with redux with defaults', () => { - const {queryByTestId} = renderWithRedux() - Simulate.click(queryByTestId('incrementer')) - expect(queryByTestId('count-value').textContent).toBe('1') + const {getByTestId} = renderWithRedux() + Simulate.click(getByTestId('incrementer')) + expect(getByTestId('count-value').textContent).toBe('1') }) test('can render with redux with custom initial state', () => { - const {queryByTestId} = renderWithRedux(, { + const {getByTestId} = renderWithRedux(, { initialState: {count: 3}, }) - Simulate.click(queryByTestId('decrementer')) - expect(queryByTestId('count-value').textContent).toBe('2') + Simulate.click(getByTestId('decrementer')) + expect(getByTestId('count-value').textContent).toBe('2') }) test('can render with redux with custom store', () => { // this is a silly store that can never be changed const store = createStore(() => ({count: 1000})) - const {queryByTestId} = renderWithRedux(, { + const {getByTestId} = renderWithRedux(, { store, }) - Simulate.click(queryByTestId('incrementer')) - expect(queryByTestId('count-value').textContent).toBe('1000') - Simulate.click(queryByTestId('decrementer')) - expect(queryByTestId('count-value').textContent).toBe('1000') + Simulate.click(getByTestId('incrementer')) + expect(getByTestId('count-value').textContent).toBe('1000') + Simulate.click(getByTestId('decrementer')) + expect(getByTestId('count-value').textContent).toBe('1000') }) diff --git a/src/__tests__/react-router.js b/src/__tests__/react-router.js index 57c2fc83..74e75584 100644 --- a/src/__tests__/react-router.js +++ b/src/__tests__/react-router.js @@ -49,11 +49,11 @@ function renderWithRouter( } test('full app rendering/navigating', () => { - const {container, queryByTestId} = renderWithRouter() + const {container, getByTestId} = renderWithRouter() // normally I'd use a data-testid, but just wanted to show this is also possible expect(container.innerHTML).toMatch('You are home') const leftClick = {button: 0} - Simulate.click(queryByTestId('about-link'), leftClick) + Simulate.click(getByTestId('about-link'), leftClick) // normally I'd use a data-testid, but just wanted to show this is also possible expect(container.innerHTML).toMatch('You are on the about page') }) @@ -68,6 +68,6 @@ test('landing on a bad page', () => { test('rendering a component that uses withRouter', () => { const route = '/some-route' - const {queryByTestId} = renderWithRouter(, {route}) - expect(queryByTestId('location-display').textContent).toBe(route) + const {getByTestId} = renderWithRouter(, {route}) + expect(getByTestId('location-display').textContent).toBe(route) }) diff --git a/src/__tests__/shallow.react-transition-group.js b/src/__tests__/shallow.react-transition-group.js index 0ff49dfb..b724eb70 100644 --- a/src/__tests__/shallow.react-transition-group.js +++ b/src/__tests__/shallow.react-transition-group.js @@ -35,7 +35,7 @@ jest.mock('react-transition-group', () => { }) test('you can mock things with jest.mock', () => { - const {queryByTestId} = render() + const {getByTestId} = render() const context = expect.any(Object) const children = expect.any(Object) const defaultProps = {children, timeout: 1000, className: 'fade'} @@ -43,7 +43,7 @@ test('you can mock things with jest.mock', () => { {in: true, ...defaultProps}, context, ) - Simulate.click(queryByTestId('toggle-message')) + Simulate.click(getByTestId('toggle-message')) expect(CSSTransition).toHaveBeenCalledWith( {in: true, ...defaultProps}, expect.any(Object), diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js index e4b15c37..66dd2667 100644 --- a/src/__tests__/stopwatch.js +++ b/src/__tests__/stopwatch.js @@ -43,8 +43,8 @@ const wait = time => new Promise(resolve => setTimeout(resolve, time)) test('unmounts a component', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}) - const {unmount, queryByTestId, container} = render() - Simulate.click(queryByTestId('start-stop-button')) + const {unmount, getByTestId, container} = render() + Simulate.click(getByTestId('start-stop-button')) unmount() // hey there reader! You don't need to have an assertion like this one // this is just me making sure that the unmount function works. diff --git a/src/index.js b/src/index.js index da3ecba5..afa67298 100644 --- a/src/index.js +++ b/src/index.js @@ -7,16 +7,26 @@ function select(id) { } // we may expose this eventually -function queryDivByTestId(div, id) { +function queryByTestId(div, id) { return div.querySelector(select(id)) } +// we may expose this eventually +function getByTestId(div, id) { + const el = queryByTestId(div, id) + if (!el) { + throw new Error(`Unable to find element by ${select(id)}`) + } + return el +} + function render(ui, {container = document.createElement('div')} = {}) { ReactDOM.render(ui, container) return { container, unmount: () => ReactDOM.unmountComponentAtNode(container), - queryByTestId: queryDivByTestId.bind(null, container), + queryByTestId: queryByTestId.bind(null, container), + getByTestId: getByTestId.bind(null, container), } } diff --git a/typings/index.d.ts b/typings/index.d.ts index ab239595..f3c8554c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -4,6 +4,7 @@ interface RenderResult { container: HTMLDivElement unmount: VoidFunction queryByTestId: (id: string) => HTMLElement | null + getByTestId: (id: string) => HTMLElement } export function render(