Skip to content

Commit 86d3508

Browse files
jomaxxKent C. Dodds
authored and
Kent C. Dodds
committed
feat(fireEvent): add fireEvent from dom-testing-library (#48)
* add fireEvent from dom-testing-library * add contributor * added docs * use document for synthetic events * added renderIntoDocument and clearDocument * added docs * update tests and README.md * fix typos * fix typo * update readme * fix typo * use beforeEach * added cleanup, removed clearDocument * use afterEach for cleanup * Update README.md
1 parent 6d9c368 commit 86d3508

File tree

5 files changed

+308
-4
lines changed

5 files changed

+308
-4
lines changed

.all-contributorsrc

+11
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@
137137
"code",
138138
"doc"
139139
]
140+
},
141+
{
142+
"login": "jomaxx",
143+
"name": "Josef Maxx Blake",
144+
"avatar_url": "https://avatars2.githubusercontent.com/u/2747424?v=4",
145+
"profile": "http://jomaxx.com",
146+
"contributions": [
147+
"code",
148+
"doc",
149+
"test"
150+
]
140151
}
141152
]
142153
}

README.md

+86-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
[![downloads][downloads-badge]][npmtrends]
1717
[![MIT License][license-badge]][license]
1818

19-
[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors)
19+
[![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors)
2020
[![PRs Welcome][prs-badge]][prs]
2121
[![Code of Conduct][coc-badge]][coc]
2222

@@ -81,8 +81,11 @@ facilitate testing implementation details). Read more about this in
8181
* [Installation](#installation)
8282
* [Usage](#usage)
8383
* [`render`](#render)
84+
* [`renderIntoDocument`](#renderintodocument)
85+
* [`cleanup`](#cleanup)
8486
* [`Simulate`](#simulate)
8587
* [`wait`](#wait)
88+
* [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event)
8689
* [`TextMatch`](#textmatch)
8790
* [`query` APIs](#query-apis)
8891
* [Examples](#examples)
@@ -264,6 +267,31 @@ const usernameInputElement = getByTestId('username-input')
264267
> Learn more about `data-testid`s from the blog post
265268
> ["Making your UI tests resilient to change"][data-testid-blog-post]
266269
270+
### `renderIntoDocument`
271+
272+
Render into `document.body`. Should be used with [cleanup](#cleanup)
273+
274+
```javascript
275+
renderIntoDocument(<div>)
276+
```
277+
278+
### `cleanup`
279+
280+
Unmounts React trees that were mounted with [renderIntoDocument](#renderintodocument).
281+
282+
```javascript
283+
afterEach(cleanup)
284+
285+
test('renders into document', () => {
286+
renderIntoDocument(<div>)
287+
// ...
288+
})
289+
```
290+
291+
Failing to call `cleanup` when you've called `renderIntoDocument` could
292+
result in a memory leak and tests which are not `idempotent` (which can
293+
lead to difficult to debug errors in your tests).
294+
267295
### `Simulate`
268296

269297
This is simply a re-export from the `Simulate` utility from
@@ -313,6 +341,62 @@ The default `interval` is `50ms`. However it will run your callback immediately
313341
on the next tick of the event loop (in a `setTimeout`) before starting the
314342
intervals.
315343

344+
### `fireEvent(node: HTMLElement, event: Event)`
345+
346+
Fire DOM events.
347+
348+
React attaches an event handler on the `document` and handles some DOM events
349+
via event delegation (events bubbling up from a `target` to an ancestor). Because
350+
of this, your `node` must be in the `document.body` for `fireEvent` to work with
351+
React. You can render into the document using the
352+
[renderIntoDocument](#renderintodocument) utility. This is an alternative to
353+
simulating Synthetic React Events via [Simulate](#simulate). The benefit of
354+
using `fireEvent` over `Simulate` is that you are testing real DOM events
355+
instead of Synthetic Events. This aligns better with
356+
[the Guiding Principles](#guiding-principles).
357+
358+
> NOTE: If you don't like having to render into the document to get `fireEvent`
359+
> working, then feel free to try to chip into making it possible for React
360+
> to attach event handlers to the rendered node rather than the `document`.
361+
> Learn more here:
362+
> [facebook/react#2043](https://github.com/facebook/react/issues/2043)
363+
364+
```javascript
365+
import { renderIntoDocument, cleanup, render, fireEvent }
366+
367+
// don't forget to clean up the document.body
368+
afterEach(cleanup)
369+
370+
test('clicks submit button', () => {
371+
const spy = jest.fn();
372+
const { unmount, getByText } = renderIntoDocument(<button onClick={spy}>Submit</button>)
373+
374+
fireEvent(
375+
getByText('Submit'),
376+
new MouseEvent('click', {
377+
bubbles: true, // click events must bubble for React to see it
378+
cancelable: true,
379+
})
380+
)
381+
382+
expect(spy).toHaveBeenCalledTimes(1)
383+
})
384+
```
385+
386+
#### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)`
387+
388+
Convenience methods for firing DOM events. Check out
389+
[dom-testing-library/src/events.js](https://github.com/kentcdodds/dom-testing-library/blob/master/src/events.js)
390+
for a full list as well as default `eventProperties`.
391+
392+
```javascript
393+
// similar to the above example
394+
// click will bubble for React to see it
395+
const rightClick = {button: 2}
396+
fireEvent.click(getElementByText('Submit'), rightClick)
397+
// default `button` property for click events is set to `0` which is a left click.
398+
```
399+
316400
## `TextMatch`
317401

318402
Several APIs accept a `TextMatch` which can be a `string`, `regex` or a
@@ -635,7 +719,7 @@ Thanks goes to these people ([emoji key][emojis]):
635719
<!-- prettier-ignore -->
636720
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[💻](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") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Paweł Mikołajczyk</b></sub>](https://github.com/Miklet)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro Ñáñez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1402095?v=4" width="100px;"/><br /><sub><b>Matt Parrish</b></sub>](https://github.com/pbomb)<br />[🐛](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") | [<img src="https://avatars1.githubusercontent.com/u/1288694?v=4" width="100px;"/><br /><sub><b>Justin Hall</b></sub>](https://github.com/wKovacs64)<br />[📦](#platform-wKovacs64 "Packaging/porting to new platform") |
637721
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
638-
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[💻](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") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](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") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💬](#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") |
722+
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[💻](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") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](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") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💬](#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") | [<img src="https://avatars2.githubusercontent.com/u/2747424?v=4" width="100px;"/><br /><sub><b>Josef Maxx Blake</b></sub>](http://jomaxx.com)<br />[💻](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") |
639723

640724
<!-- ALL-CONTRIBUTORS-LIST:END -->
641725

src/__tests__/events.js

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import React from 'react'
2+
import {renderIntoDocument, cleanup, fireEvent} from '../'
3+
4+
const eventTypes = [
5+
{
6+
type: 'Clipboard',
7+
events: ['copy', 'paste'],
8+
elementType: 'input',
9+
},
10+
{
11+
type: 'Composition',
12+
events: ['compositionEnd', 'compositionStart', 'compositionUpdate'],
13+
elementType: 'input',
14+
},
15+
{
16+
type: 'Keyboard',
17+
events: ['keyDown', 'keyPress', 'keyUp'],
18+
elementType: 'input',
19+
init: {keyCode: 13},
20+
},
21+
{
22+
type: 'Focus',
23+
events: ['focus', 'blur'],
24+
elementType: 'input',
25+
},
26+
{
27+
type: 'Form',
28+
events: ['focus', 'blur'],
29+
elementType: 'input',
30+
},
31+
{
32+
type: 'Focus',
33+
events: ['change', 'input', 'invalid'],
34+
elementType: 'input',
35+
},
36+
{
37+
type: 'Focus',
38+
events: ['submit'],
39+
elementType: 'form',
40+
},
41+
{
42+
type: 'Mouse',
43+
events: [
44+
'click',
45+
'contextMenu',
46+
'doubleClick',
47+
'drag',
48+
'dragEnd',
49+
'dragEnter',
50+
'dragExit',
51+
'dragLeave',
52+
'dragOver',
53+
'dragStart',
54+
'drop',
55+
'mouseDown',
56+
'mouseEnter',
57+
'mouseLeave',
58+
'mouseMove',
59+
'mouseOut',
60+
'mouseOver',
61+
'mouseUp',
62+
],
63+
elementType: 'button',
64+
},
65+
{
66+
type: 'Selection',
67+
events: ['select'],
68+
elementType: 'input',
69+
},
70+
{
71+
type: 'Touch',
72+
events: ['touchCancel', 'touchEnd', 'touchMove', 'touchStart'],
73+
elementType: 'button',
74+
},
75+
{
76+
type: 'UI',
77+
events: ['scroll'],
78+
elementType: 'div',
79+
},
80+
{
81+
type: 'Wheel',
82+
events: ['wheel'],
83+
elementType: 'div',
84+
},
85+
{
86+
type: 'Media',
87+
events: [
88+
'abort',
89+
'canPlay',
90+
'canPlayThrough',
91+
'durationChange',
92+
'emptied',
93+
'encrypted',
94+
'ended',
95+
'error',
96+
'loadedData',
97+
'loadedMetadata',
98+
'loadStart',
99+
'pause',
100+
'play',
101+
'playing',
102+
'progress',
103+
'rateChange',
104+
'seeked',
105+
'seeking',
106+
'stalled',
107+
'suspend',
108+
'timeUpdate',
109+
'volumeChange',
110+
'waiting',
111+
],
112+
elementType: 'video',
113+
},
114+
{
115+
type: 'Image',
116+
events: ['load', 'error'],
117+
elementType: 'img',
118+
},
119+
{
120+
type: 'Animation',
121+
events: ['animationStart', 'animationEnd', 'animationIteration'],
122+
elementType: 'div',
123+
},
124+
{
125+
type: 'Transition',
126+
events: ['transitionEnd'],
127+
elementType: 'div',
128+
},
129+
]
130+
131+
afterEach(cleanup)
132+
133+
eventTypes.forEach(({type, events, elementType, init}) => {
134+
describe(`${type} Events`, () => {
135+
events.forEach(eventName => {
136+
const propName = `on${eventName.charAt(0).toUpperCase()}${eventName.slice(
137+
1,
138+
)}`
139+
140+
it(`triggers ${propName}`, () => {
141+
const ref = React.createRef()
142+
const spy = jest.fn()
143+
144+
renderIntoDocument(
145+
React.createElement(elementType, {
146+
[propName]: spy,
147+
ref,
148+
}),
149+
)
150+
151+
fireEvent[eventName](ref.current, init)
152+
expect(spy).toHaveBeenCalledTimes(1)
153+
})
154+
})
155+
})
156+
})

src/__tests__/render-into-document.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react'
2+
import {renderIntoDocument, cleanup} from '../'
3+
4+
afterEach(cleanup)
5+
6+
it('renders button into document', () => {
7+
const ref = React.createRef()
8+
const {container} = renderIntoDocument(<div ref={ref} />)
9+
expect(container.firstChild).toBe(ref.current)
10+
})
11+
12+
it('cleansup document', () => {
13+
const spy = jest.fn()
14+
15+
class Test extends React.Component {
16+
componentWillUnmount() {
17+
spy()
18+
}
19+
20+
render() {
21+
return <div />
22+
}
23+
}
24+
25+
renderIntoDocument(<Test />)
26+
cleanup()
27+
expect(document.body.innerHTML).toBe('')
28+
expect(spy).toHaveBeenCalledTimes(1)
29+
})

src/index.js

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ReactDOM from 'react-dom'
22
import {Simulate} from 'react-dom/test-utils'
3-
import {queries, wait} from 'dom-testing-library'
3+
import {queries, wait, fireEvent} from 'dom-testing-library'
44

55
function render(ui, {container = document.createElement('div')} = {}) {
66
ReactDOM.render(ui, container)
@@ -18,4 +18,28 @@ function render(ui, {container = document.createElement('div')} = {}) {
1818
}
1919
}
2020

21-
export {render, Simulate, wait}
21+
const mountedContainers = new Set()
22+
23+
function renderIntoDocument(ui) {
24+
const container = document.body.appendChild(document.createElement('div'))
25+
mountedContainers.add(container)
26+
return render(ui, {container})
27+
}
28+
29+
function cleanup() {
30+
mountedContainers.forEach(container => {
31+
document.body.removeChild(container)
32+
ReactDOM.unmountComponentAtNode(container)
33+
mountedContainers.delete(container)
34+
})
35+
}
36+
37+
// fallback to synthetic events for React events that the DOM doesn't support
38+
const syntheticEvents = ['change', 'select', 'mouseEnter', 'mouseLeave']
39+
syntheticEvents.forEach(eventName => {
40+
document.addEventListener(eventName.toLowerCase(), e => {
41+
Simulate[eventName](e.target, e)
42+
})
43+
})
44+
45+
export {render, Simulate, wait, fireEvent, renderIntoDocument, cleanup}

0 commit comments

Comments
 (0)