From 8a10c393a42e3fa5226498be9da2ac3d2306da82 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Mon, 9 Apr 2018 22:52:29 -0400 Subject: [PATCH 01/15] add fireEvent from dom-testing-library --- src/__tests__/events.js | 153 ++++++++++++++++++++++++++++++++++++++++ src/index.js | 16 ++++- 2 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/events.js diff --git a/src/__tests__/events.js b/src/__tests__/events.js new file mode 100644 index 00000000..f220b289 --- /dev/null +++ b/src/__tests__/events.js @@ -0,0 +1,153 @@ +import React from 'react' +import {render, fireEvent} from '../' + +const eventTypes = [ + { + type: 'Clipboard', + events: ['copy', 'paste'], + elementType: 'input', + }, + { + type: 'Composition', + events: ['compositionEnd', 'compositionStart', 'compositionUpdate'], + elementType: 'input', + }, + { + type: 'Keyboard', + events: ['keyDown', 'keyPress', 'keyUp'], + elementType: 'input', + init: {keyCode: 13}, + }, + { + type: 'Focus', + events: ['focus', 'blur'], + elementType: 'input', + }, + { + type: 'Form', + events: ['focus', 'blur'], + elementType: 'input', + }, + { + type: 'Focus', + events: ['change', 'input', 'invalid'], + elementType: 'input', + }, + { + type: 'Focus', + events: ['submit'], + elementType: 'form', + }, + { + type: 'Mouse', + events: [ + 'click', + 'contextMenu', + 'doubleClick', + 'drag', + 'dragEnd', + 'dragEnter', + 'dragExit', + 'dragLeave', + 'dragOver', + 'dragStart', + 'drop', + 'mouseDown', + 'mouseEnter', + 'mouseLeave', + 'mouseMove', + 'mouseOut', + 'mouseOver', + 'mouseUp', + ], + elementType: 'button', + }, + { + type: 'Selection', + events: ['select'], + elementType: 'input', + }, + { + type: 'Touch', + events: ['touchCancel', 'touchEnd', 'touchMove', 'touchStart'], + elementType: 'button', + }, + { + type: 'UI', + events: ['scroll'], + elementType: 'div', + }, + { + type: 'Wheel', + events: ['wheel'], + elementType: 'div', + }, + { + type: 'Media', + events: [ + 'abort', + 'canPlay', + 'canPlayThrough', + 'durationChange', + 'emptied', + 'encrypted', + 'ended', + 'error', + 'loadedData', + 'loadedMetadata', + 'loadStart', + 'pause', + 'play', + 'playing', + 'progress', + 'rateChange', + 'seeked', + 'seeking', + 'stalled', + 'suspend', + 'timeUpdate', + 'volumeChange', + 'waiting', + ], + elementType: 'video', + }, + { + type: 'Image', + events: ['load', 'error'], + elementType: 'img', + }, + { + type: 'Animation', + events: ['animationStart', 'animationEnd', 'animationIteration'], + elementType: 'div', + }, + { + type: 'Transition', + events: ['transitionEnd'], + elementType: 'div', + }, +] + +eventTypes.forEach(({type, events, elementType, init}) => { + describe(`${type} Events`, () => { + events.forEach(eventName => { + const propName = `on${eventName.charAt(0).toUpperCase()}${eventName.slice( + 1, + )}` + + it(`triggers ${propName}`, () => { + let node + const spy = jest.fn() + + render( + React.createElement(elementType, { + [propName]: spy, + ref: el => (node = el), + }), + ) + fireEvent[eventName](node, init) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + }) +}) diff --git a/src/index.js b/src/index.js index 9906d080..7d2e09f9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,11 @@ import ReactDOM from 'react-dom' import {Simulate} from 'react-dom/test-utils' -import {queries, wait} from 'dom-testing-library' +import {queries, wait, fireEvent} from 'dom-testing-library' -function render(ui, {container = document.createElement('div')} = {}) { +function render( + ui, + {container = document.body.appendChild(document.createElement('div'))} = {}, +) { ReactDOM.render(ui, container) const containerHelpers = Object.entries(queries).reduce( (helpers, [key, fn]) => { @@ -18,4 +21,11 @@ function render(ui, {container = document.createElement('div')} = {}) { } } -export {render, Simulate, wait} +// fallback to synthetic events for DOM events that React doesn't handle +;['change', 'select', 'mouseEnter', 'mouseLeave'].forEach(eventName => { + window.addEventListener(eventName.toLowerCase(), e => { + Simulate[eventName](e.target, e) + }) +}) + +export {render, Simulate, wait, fireEvent} From 2fda80fbbe0ba927db47d422804717a7d88fe7c6 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Mon, 9 Apr 2018 22:57:29 -0400 Subject: [PATCH 02/15] add contributor --- .all-contributorsrc | 11 +++++++++++ README.md | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b61e4c41..7766b25e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -137,6 +137,17 @@ "code", "doc" ] + }, + { + "login": "jomaxx", + "name": "Josef Maxx Blake", + "avatar_url": "https://avatars2.githubusercontent.com/u/2747424?v=4", + "profile": "http://jomaxx.com", + "contributions": [ + "code", + "doc", + "test" + ] } ] } diff --git a/README.md b/README.md index 42ccefe7..7806b694 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-13-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -635,7 +635,7 @@ Thanks goes to these people ([emoji key][emojis]): | [
Kent C. Dodds](https://kentcdodds.com)
[πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [πŸš‡](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [
Ryan Castner](http://audiolion.github.io)
[πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [
Daniel Sandiego](https://www.dnlsandiego.com)
[πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [
PaweΕ‚ MikoΕ‚ajczyk](https://github.com/Miklet)
[πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [
Alejandro ÑÑñez Ortiz](http://co.linkedin.com/in/alejandronanez/)
[πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [
Matt Parrish](https://github.com/pbomb)
[πŸ›](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Apbomb "Bug reports") [πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Code") [πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Tests") | [
Justin Hall](https://github.com/wKovacs64)
[πŸ“¦](#platform-wKovacs64 "Packaging/porting to new platform") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| [
Anto Aravinth](https://github.com/antoaravinth)
[πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Tests") [πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | [
Łukasz Gandecki](http://team.thebrain.pro)
[πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Tests") [πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[πŸ›](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [πŸ€”](#ideas-sompylasar "Ideas, Planning, & Feedback") | [
Jesse Day](https://github.com/jday3)
[πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") | [
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") | +| [
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") | [
Josef Maxx Blake](http://jomaxx.com)
[πŸ’»](https://github.com/kentcdodds/react-testing-library/commits?author=jomaxx "Code") [πŸ“–](https://github.com/kentcdodds/react-testing-library/commits?author=jomaxx "Documentation") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=jomaxx "Tests") | From 108e29569729059f4094525967b0a445103e6112 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Mon, 9 Apr 2018 23:01:29 -0400 Subject: [PATCH 03/15] added docs --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 7806b694..a7072234 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ facilitate testing implementation details). Read more about this in * [`render`](#render) * [`Simulate`](#simulate) * [`wait`](#wait) + * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Examples](#examples) @@ -313,6 +314,30 @@ 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. +### `fireEvent(node: HTMLElement, event: Event)` + +Fire DOM events. + +```javascript +// +fireEvent( + getElementByText('Submit'), + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }), +) +``` + +#### `fireEvent[eventName](node: HTMLElement, eventInit)` + +Convenience methods for firing DOM events. Look [here](./src/events.js) for full list. + +```javascript +// +fireEvent.click(getElementByText('Submit')) +``` + ## `TextMatch` Several APIs accept a `TextMatch` which can be a `string`, `regex` or a From 7f7daa68915d7639ff3f00a88b2f6335efecf234 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 00:08:16 -0400 Subject: [PATCH 04/15] use document for synthetic events --- src/__tests__/events.js | 7 +++++++ src/index.js | 10 ++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/__tests__/events.js b/src/__tests__/events.js index f220b289..999980dd 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -128,6 +128,10 @@ const eventTypes = [ }, ] +afterEach(() => { + document.body.innerHTML = '' +}) + eventTypes.forEach(({type, events, elementType, init}) => { describe(`${type} Events`, () => { events.forEach(eventName => { @@ -144,6 +148,9 @@ eventTypes.forEach(({type, events, elementType, init}) => { [propName]: spy, ref: el => (node = el), }), + { + container: document.body.appendChild(document.createElement('div')), + }, ) fireEvent[eventName](node, init) expect(spy).toHaveBeenCalledTimes(1) diff --git a/src/index.js b/src/index.js index 7d2e09f9..a77ae00d 100644 --- a/src/index.js +++ b/src/index.js @@ -2,10 +2,7 @@ import ReactDOM from 'react-dom' import {Simulate} from 'react-dom/test-utils' import {queries, wait, fireEvent} from 'dom-testing-library' -function render( - ui, - {container = document.body.appendChild(document.createElement('div'))} = {}, -) { +function render(ui, {container = document.createElement('div')} = {}) { ReactDOM.render(ui, container) const containerHelpers = Object.entries(queries).reduce( (helpers, [key, fn]) => { @@ -22,8 +19,9 @@ function render( } // fallback to synthetic events for DOM events that React doesn't handle -;['change', 'select', 'mouseEnter', 'mouseLeave'].forEach(eventName => { - window.addEventListener(eventName.toLowerCase(), e => { +const syntheticEvents = ['change', 'select', 'mouseEnter', 'mouseLeave'] +syntheticEvents.forEach(eventName => { + document.addEventListener(eventName.toLowerCase(), e => { Simulate[eventName](e.target, e) }) }) From 2381249c4363d8682d92c5d0348fd5a3ff35175d Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 08:41:52 -0400 Subject: [PATCH 05/15] added renderIntoDocument and clearDocument --- src/__tests__/events.js | 18 +++++++----------- src/__tests__/renderIntoDocument.js | 16 ++++++++++++++++ src/index.js | 12 +++++++++++- 3 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 src/__tests__/renderIntoDocument.js diff --git a/src/__tests__/events.js b/src/__tests__/events.js index 999980dd..26075fb0 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -1,5 +1,5 @@ import React from 'react' -import {render, fireEvent} from '../' +import {renderIntoDocument, clearDocument, fireEvent} from '../' const eventTypes = [ { @@ -128,9 +128,7 @@ const eventTypes = [ }, ] -afterEach(() => { - document.body.innerHTML = '' -}) +afterEach(clearDocument) eventTypes.forEach(({type, events, elementType, init}) => { describe(`${type} Events`, () => { @@ -140,19 +138,17 @@ eventTypes.forEach(({type, events, elementType, init}) => { )}` it(`triggers ${propName}`, () => { - let node + const ref = React.createRef() const spy = jest.fn() - render( + renderIntoDocument( React.createElement(elementType, { [propName]: spy, - ref: el => (node = el), + ref, }), - { - container: document.body.appendChild(document.createElement('div')), - }, ) - fireEvent[eventName](node, init) + + fireEvent[eventName](ref.current, init) expect(spy).toHaveBeenCalledTimes(1) }) }) diff --git a/src/__tests__/renderIntoDocument.js b/src/__tests__/renderIntoDocument.js new file mode 100644 index 00000000..3b3c50e9 --- /dev/null +++ b/src/__tests__/renderIntoDocument.js @@ -0,0 +1,16 @@ +import React from 'react' +import {renderIntoDocument, clearDocument} from '../' + +afterEach(clearDocument) + +it('renders button into document', () => { + const ref = React.createRef() + renderIntoDocument(
) + expect(document.body.querySelector('#test')).toBe(ref.current) +}) + +it('clears document body', () => { + renderIntoDocument(
) + clearDocument() + expect(document.body.innerHTML).toBe('') +}) diff --git a/src/index.js b/src/index.js index a77ae00d..5aeed817 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,16 @@ function render(ui, {container = document.createElement('div')} = {}) { } } +function renderIntoDocument(ui) { + return render(ui, { + container: document.body.appendChild(document.createElement('div')), + }) +} + +function clearDocument() { + document.body.innerHTML = '' +} + // fallback to synthetic events for DOM events that React doesn't handle const syntheticEvents = ['change', 'select', 'mouseEnter', 'mouseLeave'] syntheticEvents.forEach(eventName => { @@ -26,4 +36,4 @@ syntheticEvents.forEach(eventName => { }) }) -export {render, Simulate, wait, fireEvent} +export {render, Simulate, wait, fireEvent, renderIntoDocument, clearDocument} From 6cbbce9543f78775911d679cbcb2e92ab7c4b790 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:03:26 -0400 Subject: [PATCH 06/15] added docs --- README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a7072234..ebf62718 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ facilitate testing implementation details). Read more about this in * [Installation](#installation) * [Usage](#usage) * [`render`](#render) + * [`renderIntoDocument`](#renderintodocument) + * [`clearDocument`](#cleardocument) * [`Simulate`](#simulate) * [`wait`](#wait) * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event) @@ -265,6 +267,27 @@ 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] +### `renderIntoDocument` + +Render into `document.body`. Should be used with [clearDocument](#cleardocument) + +```javascript +renderIntoDocument(
) +``` + +### `clearDocument` + +Clears the `document.body`. Good for preventing memory leaks. Should be used with [renderIntoDocument](#renderintodocument) + +```javascript +afterEach(clearDocument) + +test('renders into document', () => { + renderIntoDocument(
) + // ... +}) +``` + ### `Simulate` This is simply a re-export from the `Simulate` utility from @@ -318,15 +341,31 @@ intervals. Fire DOM events. +React attaches an event handler on the `document` and handles some DOM events via event delegation (events bubbling up from a `target` to an ancestor). Because of this, your `node` must be in the `document.body` for `fireEvent` to work with React. You can render into the document using the [renderIntoDocument](#renderintodocument) utility. This is an alternative to simulating Synthetic React Events via [Simulate](#simulate). The benefit of using `fireEvent` over `Simulate` is that you are testing real DOM events instead of Synthetic Events. This aligns better with [the Guiding Principles](#guiding-principles). + ```javascript -// -fireEvent( - getElementByText('Submit'), - new MouseEvent('click', { - bubbles: true, - cancelable: true, - }), -) +import { renderIntoDocument, clearDocument, render, fireEvent } + +// don't forget to clean up the document.body +afterEach(clearDocument) + +test('clicks submit button', () => { + const spy = jest.fn(); + const { unmount, getByText } render() + + fireEvent( + getByText('Submit'), + new MouseEvent('click', { + bubbles: true, // click events must bubble for React to see it + cancelable: true, + }) + ) + + // don't forget to unmount component so componentWillUnmount can clean up subscriptions + unmount(); + + expect(spy).toHaveBeenCalledTimes(1); +}); ``` #### `fireEvent[eventName](node: HTMLElement, eventInit)` @@ -334,8 +373,23 @@ fireEvent( Convenience methods for firing DOM events. Look [here](./src/events.js) for full list. ```javascript -// -fireEvent.click(getElementByText('Submit')) +import { renderIntoDocument, clearDocument, render, fireEvent } + +// don't forget to clean up the document.body +afterEach(clearDocument) + +test('clicks submit button', () => { + const spy = jest.fn(); + const { unmount, getByText } render() + + // click will bubble for React to see it + fireEvent.click(getByText('Submit')) + + // don't forget to unmount component so componentWillUnmount can clean up subscriptions + unmount(); + + expect(spy).toHaveBeenCalledTimes(1); +}); ``` ## `TextMatch` From cc2310b5398ab0023b51b2b82403f30c3265c3e8 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:08:52 -0400 Subject: [PATCH 07/15] update tests and README.md --- README.md | 10 +++++++--- .../{renderIntoDocument.js => render-into-document.js} | 6 +++--- src/index.js | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) rename src/__tests__/{renderIntoDocument.js => render-into-document.js} (64%) diff --git a/README.md b/README.md index ebf62718..ae34fc90 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,9 @@ test('clicks submit button', () => { #### `fireEvent[eventName](node: HTMLElement, eventInit)` -Convenience methods for firing DOM events. Look [here](./src/events.js) for full list. +Convenience methods for firing DOM events. Check out +[dom-testing-library/src/events.js](https://github.com/kentcdodds/dom-testing-library/blob/master/src/events.js) +for a full list as well as default `eventProperties`. ```javascript import { renderIntoDocument, clearDocument, render, fireEvent } @@ -380,10 +382,12 @@ afterEach(clearDocument) test('clicks submit button', () => { const spy = jest.fn(); - const { unmount, getByText } render() + const { unmount, getByText } render() // click will bubble for React to see it - fireEvent.click(getByText('Submit')) + const rightClick = {button: 2} + fireEvent.click(getElementByText('Submit'), rightClick) + // default `button` property for click events is set to `0` which is a left click. // don't forget to unmount component so componentWillUnmount can clean up subscriptions unmount(); diff --git a/src/__tests__/renderIntoDocument.js b/src/__tests__/render-into-document.js similarity index 64% rename from src/__tests__/renderIntoDocument.js rename to src/__tests__/render-into-document.js index 3b3c50e9..cbadffee 100644 --- a/src/__tests__/renderIntoDocument.js +++ b/src/__tests__/render-into-document.js @@ -5,12 +5,12 @@ afterEach(clearDocument) it('renders button into document', () => { const ref = React.createRef() - renderIntoDocument(
) - expect(document.body.querySelector('#test')).toBe(ref.current) + const {container} = renderIntoDocument(
) + expect(container.firstChild).toBe(ref.current) }) it('clears document body', () => { - renderIntoDocument(
) + renderIntoDocument(
) clearDocument() expect(document.body.innerHTML).toBe('') }) diff --git a/src/index.js b/src/index.js index 5aeed817..72f63da2 100644 --- a/src/index.js +++ b/src/index.js @@ -28,7 +28,7 @@ function clearDocument() { document.body.innerHTML = '' } -// fallback to synthetic events for DOM events that React doesn't handle +// fallback to synthetic events for React events that the DOM doesn't support const syntheticEvents = ['change', 'select', 'mouseEnter', 'mouseLeave'] syntheticEvents.forEach(eventName => { document.addEventListener(eventName.toLowerCase(), e => { From eb482e755761b164e2026cc084f5212fe714d93d Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:10:26 -0400 Subject: [PATCH 08/15] fix typos --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae34fc90..d809a56a 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ afterEach(clearDocument) test('clicks submit button', () => { const spy = jest.fn(); - const { unmount, getByText } render() + const { unmount, getByText } = render() fireEvent( getByText('Submit'), @@ -382,7 +382,8 @@ afterEach(clearDocument) test('clicks submit button', () => { const spy = jest.fn(); - const { unmount, getByText } render() + const onClick = e => e.button === 2 && spy(); + const { unmount, getByText } = render() // click will bubble for React to see it const rightClick = {button: 2} From 32f7560cbaacd3f68141f82eb59e3da3ffcbb36a Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:12:39 -0400 Subject: [PATCH 09/15] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d809a56a..6212eac1 100644 --- a/README.md +++ b/README.md @@ -368,7 +368,7 @@ test('clicks submit button', () => { }); ``` -#### `fireEvent[eventName](node: HTMLElement, eventInit)` +#### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)` Convenience methods for firing DOM events. Check out [dom-testing-library/src/events.js](https://github.com/kentcdodds/dom-testing-library/blob/master/src/events.js) From 8304913954dfcc8cc7f2d908597a5307541d3158 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:13:22 -0400 Subject: [PATCH 10/15] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6212eac1..33f2af4f 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ import { renderIntoDocument, clearDocument, render, fireEvent } // don't forget to clean up the document.body afterEach(clearDocument) -test('clicks submit button', () => { +test('right clicks submit button', () => { const spy = jest.fn(); const onClick = e => e.button === 2 && spy(); const { unmount, getByText } = render() From 9006f0e01bbbeb0e1ed32c36f88344e9ca7e7298 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:33:44 -0400 Subject: [PATCH 11/15] fix typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33f2af4f..3269d1d9 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ afterEach(clearDocument) test('clicks submit button', () => { const spy = jest.fn(); - const { unmount, getByText } = render() + const { unmount, getByText } = renderIntoDocument() fireEvent( getByText('Submit'), @@ -383,7 +383,7 @@ afterEach(clearDocument) test('right clicks submit button', () => { const spy = jest.fn(); const onClick = e => e.button === 2 && spy(); - const { unmount, getByText } = render() + const { unmount, getByText } = renderIntoDocument() // click will bubble for React to see it const rightClick = {button: 2} From 04fec2ee83b835d254596f0179ade3c88ef2e9b1 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:35:46 -0400 Subject: [PATCH 12/15] use beforeEach --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3269d1d9..f18023db 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,7 @@ React attaches an event handler on the `document` and handles some DOM events vi import { renderIntoDocument, clearDocument, render, fireEvent } // don't forget to clean up the document.body -afterEach(clearDocument) +beforeEach(clearDocument) test('clicks submit button', () => { const spy = jest.fn(); @@ -378,7 +378,7 @@ for a full list as well as default `eventProperties`. import { renderIntoDocument, clearDocument, render, fireEvent } // don't forget to clean up the document.body -afterEach(clearDocument) +beforeEach(clearDocument) test('right clicks submit button', () => { const spy = jest.fn(); From a85fad9434ed8a96023086b50caf94ca2d04ef29 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 09:46:49 -0400 Subject: [PATCH 13/15] added cleanup, removed clearDocument --- README.md | 24 +++++++++--------------- src/__tests__/events.js | 4 ++-- src/__tests__/render-into-document.js | 23 ++++++++++++++++++----- src/index.js | 18 ++++++++++++------ 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f18023db..2ec687c3 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ facilitate testing implementation details). Read more about this in * [Usage](#usage) * [`render`](#render) * [`renderIntoDocument`](#renderintodocument) - * [`clearDocument`](#cleardocument) + * [`cleanup`](#cleanup) * [`Simulate`](#simulate) * [`wait`](#wait) * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event) @@ -269,18 +269,18 @@ const usernameInputElement = getByTestId('username-input') ### `renderIntoDocument` -Render into `document.body`. Should be used with [clearDocument](#cleardocument) +Render into `document.body`. Should be used with [cleanup](#cleanup) ```javascript renderIntoDocument(
) ``` -### `clearDocument` +### `cleanup` -Clears the `document.body`. Good for preventing memory leaks. Should be used with [renderIntoDocument](#renderintodocument) +Unmounts React trees that were mounted with [renderIntoDocument](#renderintodocument). ```javascript -afterEach(clearDocument) +beforeEach(cleanup) test('renders into document', () => { renderIntoDocument(
) @@ -344,10 +344,10 @@ Fire DOM events. React attaches an event handler on the `document` and handles some DOM events via event delegation (events bubbling up from a `target` to an ancestor). Because of this, your `node` must be in the `document.body` for `fireEvent` to work with React. You can render into the document using the [renderIntoDocument](#renderintodocument) utility. This is an alternative to simulating Synthetic React Events via [Simulate](#simulate). The benefit of using `fireEvent` over `Simulate` is that you are testing real DOM events instead of Synthetic Events. This aligns better with [the Guiding Principles](#guiding-principles). ```javascript -import { renderIntoDocument, clearDocument, render, fireEvent } +import { renderIntoDocument, cleanup, render, fireEvent } // don't forget to clean up the document.body -beforeEach(clearDocument) +beforeEach(cleanup) test('clicks submit button', () => { const spy = jest.fn(); @@ -361,9 +361,6 @@ test('clicks submit button', () => { }) ) - // don't forget to unmount component so componentWillUnmount can clean up subscriptions - unmount(); - expect(spy).toHaveBeenCalledTimes(1); }); ``` @@ -375,10 +372,10 @@ Convenience methods for firing DOM events. Check out for a full list as well as default `eventProperties`. ```javascript -import { renderIntoDocument, clearDocument, render, fireEvent } +import { renderIntoDocument, cleanup, render, fireEvent } // don't forget to clean up the document.body -beforeEach(clearDocument) +beforeEach(cleanup) test('right clicks submit button', () => { const spy = jest.fn(); @@ -390,9 +387,6 @@ test('right clicks submit button', () => { fireEvent.click(getElementByText('Submit'), rightClick) // default `button` property for click events is set to `0` which is a left click. - // don't forget to unmount component so componentWillUnmount can clean up subscriptions - unmount(); - expect(spy).toHaveBeenCalledTimes(1); }); ``` diff --git a/src/__tests__/events.js b/src/__tests__/events.js index 26075fb0..25fd3e5c 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -1,5 +1,5 @@ import React from 'react' -import {renderIntoDocument, clearDocument, fireEvent} from '../' +import {renderIntoDocument, cleanup, fireEvent} from '../' const eventTypes = [ { @@ -128,7 +128,7 @@ const eventTypes = [ }, ] -afterEach(clearDocument) +beforeEach(cleanup) eventTypes.forEach(({type, events, elementType, init}) => { describe(`${type} Events`, () => { diff --git a/src/__tests__/render-into-document.js b/src/__tests__/render-into-document.js index cbadffee..62478ed6 100644 --- a/src/__tests__/render-into-document.js +++ b/src/__tests__/render-into-document.js @@ -1,7 +1,7 @@ import React from 'react' -import {renderIntoDocument, clearDocument} from '../' +import {renderIntoDocument, cleanup} from '../' -afterEach(clearDocument) +beforeEach(cleanup) it('renders button into document', () => { const ref = React.createRef() @@ -9,8 +9,21 @@ it('renders button into document', () => { expect(container.firstChild).toBe(ref.current) }) -it('clears document body', () => { - renderIntoDocument(
) - clearDocument() +it('cleansup document', () => { + const spy = jest.fn() + + class Test extends React.Component { + componentWillUnmount() { + spy() + } + + render() { + return
+ } + } + + renderIntoDocument() + cleanup() expect(document.body.innerHTML).toBe('') + expect(spy).toHaveBeenCalledTimes(1) }) diff --git a/src/index.js b/src/index.js index 72f63da2..f9200e48 100644 --- a/src/index.js +++ b/src/index.js @@ -18,14 +18,20 @@ function render(ui, {container = document.createElement('div')} = {}) { } } +const mountedContainers = new Set() + function renderIntoDocument(ui) { - return render(ui, { - container: document.body.appendChild(document.createElement('div')), - }) + const container = document.body.appendChild(document.createElement('div')) + mountedContainers.add(container) + return render(ui, {container}) } -function clearDocument() { - document.body.innerHTML = '' +function cleanup() { + mountedContainers.forEach(container => { + document.body.removeChild(container) + ReactDOM.unmountComponentAtNode(container) + mountedContainers.delete(container) + }) } // fallback to synthetic events for React events that the DOM doesn't support @@ -36,4 +42,4 @@ syntheticEvents.forEach(eventName => { }) }) -export {render, Simulate, wait, fireEvent, renderIntoDocument, clearDocument} +export {render, Simulate, wait, fireEvent, renderIntoDocument, cleanup} From e210c0c296f4cfdae4dd3b6a771c92b9eaef04a6 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Tue, 10 Apr 2018 10:35:56 -0400 Subject: [PATCH 14/15] use afterEach for cleanup --- README.md | 6 +++--- src/__tests__/events.js | 2 +- src/__tests__/render-into-document.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2ec687c3..4a6927d4 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ renderIntoDocument(
) Unmounts React trees that were mounted with [renderIntoDocument](#renderintodocument). ```javascript -beforeEach(cleanup) +afterEach(cleanup) test('renders into document', () => { renderIntoDocument(
) @@ -347,7 +347,7 @@ React attaches an event handler on the `document` and handles some DOM events vi import { renderIntoDocument, cleanup, render, fireEvent } // don't forget to clean up the document.body -beforeEach(cleanup) +afterEach(cleanup) test('clicks submit button', () => { const spy = jest.fn(); @@ -375,7 +375,7 @@ for a full list as well as default `eventProperties`. import { renderIntoDocument, cleanup, render, fireEvent } // don't forget to clean up the document.body -beforeEach(cleanup) +afterEach(cleanup) test('right clicks submit button', () => { const spy = jest.fn(); diff --git a/src/__tests__/events.js b/src/__tests__/events.js index 25fd3e5c..73fd1175 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -128,7 +128,7 @@ const eventTypes = [ }, ] -beforeEach(cleanup) +afterEach(cleanup) eventTypes.forEach(({type, events, elementType, init}) => { describe(`${type} Events`, () => { diff --git a/src/__tests__/render-into-document.js b/src/__tests__/render-into-document.js index 62478ed6..886e239a 100644 --- a/src/__tests__/render-into-document.js +++ b/src/__tests__/render-into-document.js @@ -1,7 +1,7 @@ import React from 'react' import {renderIntoDocument, cleanup} from '../' -beforeEach(cleanup) +afterEach(cleanup) it('renders button into document', () => { const ref = React.createRef() From 404e14b4eaacb8314e1bdb8d4d3e29ed52981688 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Tue, 10 Apr 2018 10:30:50 -0600 Subject: [PATCH 15/15] Update README.md --- README.md | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4a6927d4..19b999f7 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,10 @@ test('renders into document', () => { }) ``` +Failing to call `cleanup` when you've called `renderIntoDocument` could +result in a memory leak and tests which are not `idempotent` (which can +lead to difficult to debug errors in your tests). + ### `Simulate` This is simply a re-export from the `Simulate` utility from @@ -341,7 +345,21 @@ intervals. Fire DOM events. -React attaches an event handler on the `document` and handles some DOM events via event delegation (events bubbling up from a `target` to an ancestor). Because of this, your `node` must be in the `document.body` for `fireEvent` to work with React. You can render into the document using the [renderIntoDocument](#renderintodocument) utility. This is an alternative to simulating Synthetic React Events via [Simulate](#simulate). The benefit of using `fireEvent` over `Simulate` is that you are testing real DOM events instead of Synthetic Events. This aligns better with [the Guiding Principles](#guiding-principles). +React attaches an event handler on the `document` and handles some DOM events +via event delegation (events bubbling up from a `target` to an ancestor). Because +of this, your `node` must be in the `document.body` for `fireEvent` to work with +React. You can render into the document using the +[renderIntoDocument](#renderintodocument) utility. This is an alternative to +simulating Synthetic React Events via [Simulate](#simulate). The benefit of +using `fireEvent` over `Simulate` is that you are testing real DOM events +instead of Synthetic Events. This aligns better with +[the Guiding Principles](#guiding-principles). + +> NOTE: If you don't like having to render into the document to get `fireEvent` +> working, then feel free to try to chip into making it possible for React +> to attach event handlers to the rendered node rather than the `document`. +> Learn more here: +> [facebook/react#2043](https://github.com/facebook/react/issues/2043) ```javascript import { renderIntoDocument, cleanup, render, fireEvent } @@ -361,8 +379,8 @@ test('clicks submit button', () => { }) ) - expect(spy).toHaveBeenCalledTimes(1); -}); + expect(spy).toHaveBeenCalledTimes(1) +}) ``` #### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)` @@ -372,23 +390,11 @@ Convenience methods for firing DOM events. Check out for a full list as well as default `eventProperties`. ```javascript -import { renderIntoDocument, cleanup, render, fireEvent } - -// don't forget to clean up the document.body -afterEach(cleanup) - -test('right clicks submit button', () => { - const spy = jest.fn(); - const onClick = e => e.button === 2 && spy(); - const { unmount, getByText } = renderIntoDocument() - - // click will bubble for React to see it - const rightClick = {button: 2} - fireEvent.click(getElementByText('Submit'), rightClick) - // default `button` property for click events is set to `0` which is a left click. - - expect(spy).toHaveBeenCalledTimes(1); -}); +// similar to the above example +// click will bubble for React to see it +const rightClick = {button: 2} +fireEvent.click(getElementByText('Submit'), rightClick) +// default `button` property for click events is set to `0` which is a left click. ``` ## `TextMatch`