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..19b999f7 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] @@ -81,8 +81,11 @@ facilitate testing implementation details). Read more about this in * [Installation](#installation) * [Usage](#usage) * [`render`](#render) + * [`renderIntoDocument`](#renderintodocument) + * [`cleanup`](#cleanup) * [`Simulate`](#simulate) * [`wait`](#wait) + * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) * [Examples](#examples) @@ -264,6 +267,31 @@ 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 [cleanup](#cleanup) + +```javascript +renderIntoDocument(
) +``` + +### `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}