)
+```
+
+### `cleanup`
+
+Unmounts React trees that were mounted with [renderIntoDocument](#renderintodocument).
+
+```javascript
+afterEach(cleanup)
+
+test('renders into document', () => {
+ renderIntoDocument(
)
+ // ...
+})
+```
+
+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
@@ -313,6 +341,62 @@ 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.
+
+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 }
+
+// don't forget to clean up the document.body
+afterEach(cleanup)
+
+test('clicks submit button', () => {
+ const spy = jest.fn();
+ const { unmount, getByText } = renderIntoDocument(
)
+
+ fireEvent(
+ getByText('Submit'),
+ new MouseEvent('click', {
+ bubbles: true, // click events must bubble for React to see it
+ cancelable: true,
+ })
+ )
+
+ expect(spy).toHaveBeenCalledTimes(1)
+})
+```
+
+#### `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)
+for a full list as well as default `eventProperties`.
+
+```javascript
+// 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`
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]):
| [
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") |
diff --git a/src/__tests__/events.js b/src/__tests__/events.js
new file mode 100644
index 00000000..73fd1175
--- /dev/null
+++ b/src/__tests__/events.js
@@ -0,0 +1,156 @@
+import React from 'react'
+import {renderIntoDocument, cleanup, 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',
+ },
+]
+
+afterEach(cleanup)
+
+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}`, () => {
+ const ref = React.createRef()
+ const spy = jest.fn()
+
+ renderIntoDocument(
+ React.createElement(elementType, {
+ [propName]: spy,
+ ref,
+ }),
+ )
+
+ fireEvent[eventName](ref.current, init)
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+ })
+ })
+})
diff --git a/src/__tests__/render-into-document.js b/src/__tests__/render-into-document.js
new file mode 100644
index 00000000..886e239a
--- /dev/null
+++ b/src/__tests__/render-into-document.js
@@ -0,0 +1,29 @@
+import React from 'react'
+import {renderIntoDocument, cleanup} from '../'
+
+afterEach(cleanup)
+
+it('renders button into document', () => {
+ const ref = React.createRef()
+ const {container} = renderIntoDocument(
)
+ expect(container.firstChild).toBe(ref.current)
+})
+
+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 9906d080..f9200e48 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,6 @@
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')} = {}) {
ReactDOM.render(ui, container)
@@ -18,4 +18,28 @@ function render(ui, {container = document.createElement('div')} = {}) {
}
}
-export {render, Simulate, wait}
+const mountedContainers = new Set()
+
+function renderIntoDocument(ui) {
+ const container = document.body.appendChild(document.createElement('div'))
+ mountedContainers.add(container)
+ return render(ui, {container})
+}
+
+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
+const syntheticEvents = ['change', 'select', 'mouseEnter', 'mouseLeave']
+syntheticEvents.forEach(eventName => {
+ document.addEventListener(eventName.toLowerCase(), e => {
+ Simulate[eventName](e.target, e)
+ })
+})
+
+export {render, Simulate, wait, fireEvent, renderIntoDocument, cleanup}