From 9d0a07f9abd525a63070694c66e6adecec0727ff Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 19 Mar 2018 15:49:06 -0600 Subject: [PATCH] feat(unmount): add `unmount` to render object --- README.md | 19 +++++++++++- src/__tests__/stopwatch.js | 59 ++++++++++++++++++++++++++++++++++++++ src/index.js | 1 + 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/stopwatch.js diff --git a/README.md b/README.md index 26d9980e..1854178f 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,21 @@ The containing DOM node of your rendered React Element (rendered using > Tip: To get the root element of your rendered element, use `container.firstChild`. +#### `unmount` + +This will cause the rendered component to be unmounted. This is useful for +testing what happens when your component is removed from the page (like testing +that you don't leave event handlers hanging around causing memory leaks). + +> This method is a pretty small abstraction over +> `ReactDOM.unmountComponentAtNode` + +```javascript +const {container, unmount} = render() +unmount() +// your component has been unmounted and now: container.innerHTML === '' +``` + #### `queryByTestId` A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. Read @@ -270,7 +285,9 @@ Or you could include the index or an ID in your attribute: And then you could use the `queryByTestId`: ```javascript -const items = [/* your items */] +const items = [ + /* your items */ +] const {queryByTestId} = render(/* your component with the items */) const thirdItem = queryByTestId(`item-${items[2].id}`) ``` diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js new file mode 100644 index 00000000..e4b15c37 --- /dev/null +++ b/src/__tests__/stopwatch.js @@ -0,0 +1,59 @@ +import React from 'react' +import {render, Simulate} from '../' + +class StopWatch extends React.Component { + state = {lapse: 0, running: false} + handleRunClick = () => { + this.setState(state => { + if (state.running) { + clearInterval(this.timer) + } else { + const startTime = Date.now() - this.state.lapse + this.timer = setInterval(() => { + this.setState({lapse: Date.now() - startTime}) + }) + } + return {running: !state.running} + }) + } + handleClearClick = () => { + clearInterval(this.timer) + this.setState({lapse: 0, running: false}) + } + componentWillUnmount() { + clearInterval(this.timer) + } + render() { + const {lapse, running} = this.state + return ( +
+ {lapse}ms + + +
+ ) + } +} + +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')) + 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. + // You don't need to do this in your apps. Just rely on the fact that this works. + expect(container.innerHTML).toBe('') + // just wait to see if the interval is cleared or not + // if it's not, then we'll call setState on an unmounted component + // and get an error. + await wait() + // eslint-disable-next-line no-console + expect(console.error).not.toHaveBeenCalled() +}) diff --git a/src/index.js b/src/index.js index 7d99459d..b1258b96 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,7 @@ function render(ui, {container = document.createElement('div')} = {}) { ReactDOM.render(ui, container) return { container, + unmount: () => ReactDOM.unmountComponentAtNode(container), queryByTestId: queryDivByTestId.bind(null, container), } }