From 1c4cff7d6b92dba81c67f9e2c0ea17de126fcc0b Mon Sep 17 00:00:00 2001 From: Leland Richardson Date: Thu, 7 Jan 2016 12:14:22 -0800 Subject: [PATCH] Added mount/unmount to ReactWrapper --- docs/README.md | 2 + docs/api/ReactWrapper/mount.md | 51 +++++++++++++++++++++++ docs/api/ReactWrapper/unmount.md | 47 +++++++++++++++++++++ docs/api/mount.md | 6 +++ package.json | 6 +-- src/ReactWrapper.js | 32 +++++++++++++++ src/ReactWrapperComponent.jsx | 5 ++- src/__tests__/ReactWrapper-spec.js | 65 +++++++++++++++++++++++++++++- src/__tests__/Utils-spec.js | 8 ++-- src/__tests__/_helpers.js | 11 +++++ withDom.js | 22 ++++++++++ 11 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 docs/api/ReactWrapper/mount.md create mode 100644 docs/api/ReactWrapper/unmount.md create mode 100644 withDom.js diff --git a/docs/README.md b/docs/README.md index 4433eb9f2..21fd17df4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,6 +76,8 @@ * [setProps(nextProps)](/docs/api/ReactWrapper/setProps.md) * [setContext(context)](/docs/api/ReactWrapper/setContext.md) * [instance()](/docs/api/ReactWrapper/instance.md) + * [unmount()](/docs/api/ReactWrapper/unmount.md) + * [mount()](/docs/api/ReactWrapper/mount.md) * [update()](/docs/api/ReactWrapper/update.md) * [type()](/docs/api/ReactWrapper/type.md) * [forEach(fn)](/docs/api/ReactWrapper/forEach.md) diff --git a/docs/api/ReactWrapper/mount.md b/docs/api/ReactWrapper/mount.md new file mode 100644 index 000000000..55a493964 --- /dev/null +++ b/docs/api/ReactWrapper/mount.md @@ -0,0 +1,51 @@ +# `.mount() => Self` + +A method that re-mounts the component. This can be used to simulate a component going through +an unmount/mount lifecycle. + +#### Returns + +`ReactWrapper`: Returns itself. + + + +#### Example + +```jsx +const willMount = sinon.spy(); +const didMount = sinon.spy(); +const willUnmount = sinon.spy(); + +class Foo extends React.Component { + constructor(props) { + super(props); + this.componentWillUnmount = willUnmount; + this.componentWillMount = willMount; + this.componentDidMount = didMount; + } + render() { + return ( +
+ {this.props.id} +
+ ); + } +} +const wrapper = mount(); +expect(willMount.callCount).to.equal(1); +expect(didMount.callCount).to.equal(1); +expect(willUnmount.callCount).to.equal(0); +wrapper.unmount(); +expect(willMount.callCount).to.equal(1); +expect(didMount.callCount).to.equal(1); +expect(willUnmount.callCount).to.equal(1); +wrapper.mount(); +expect(willMount.callCount).to.equal(2); +expect(didMount.callCount).to.equal(2); +expect(willUnmount.callCount).to.equal(1); +``` + + +#### Related Methods + +- [`.unmount() => Self`](unmount.md) diff --git a/docs/api/ReactWrapper/unmount.md b/docs/api/ReactWrapper/unmount.md new file mode 100644 index 000000000..67e4fa10b --- /dev/null +++ b/docs/api/ReactWrapper/unmount.md @@ -0,0 +1,47 @@ +# `.unmount() => Self` + +A method that re-mounts the component. This can be used to simulate a component going through +an unmount/mount lifecycle. + +#### Returns + +`ReactWrapper`: Returns itself. + + + +#### Example + +```jsx +const willMount = sinon.spy(); +const didMount = sinon.spy(); +const willUnmount = sinon.spy(); + +class Foo extends React.Component { + constructor(props) { + super(props); + this.componentWillUnmount = willUnmount; + this.componentWillMount = willMount; + this.componentDidMount = didMount; + } + render() { + return ( +
+ {this.props.id} +
+ ); + } +} +const wrapper = mount(); +expect(willMount.callCount).to.equal(1); +expect(didMount.callCount).to.equal(1); +expect(willUnmount.callCount).to.equal(0); +wrapper.unmount(); +expect(willMount.callCount).to.equal(1); +expect(didMount.callCount).to.equal(1); +expect(willUnmount.callCount).to.equal(1); +``` + + +#### Related Methods + +- [`.mount() => Self`](mount.md) diff --git a/docs/api/mount.md b/docs/api/mount.md index cb14bbb02..5aeb61eb1 100644 --- a/docs/api/mount.md +++ b/docs/api/mount.md @@ -133,6 +133,12 @@ Manually sets context of the root component. #### [`.instance() => ReactComponent`](ReactWrapper/instance.md) Returns the instance of the root component. +#### [`.unmount() => ReactWrapper`](ReactWrapper/unmount.md) +A method that un-mounts the component. + +#### [`.mount() => ReactWrapper`](ReactWrapper/mount.md) +A method that re-mounts the component. + #### [`.update() => ReactWrapper`](ReactWrapper/update.md) Calls `.forceUpdate()` on the root component instance. diff --git a/package.json b/package.json index a7ae60dd3..09051cb98 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ "version": "npm run build", "clean": "rimraf build", "lint": "eslint src/**", - "test": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/*.js", + "test": "mocha --compilers js:babel-core/register --recursive withDom.js src/**/__tests__/*.js", "check": "npm run lint && npm run test:all", "build": "babel src --out-dir build", - "test:watch": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/*.js --watch", - "test:only": "mocha --compilers js:babel-core/register --watch", + "test:only": "mocha --compilers js:babel-core/register --watch withDom.js", + "test:watch": "mocha --compilers js:babel-core/register --recursive withDom.js src/**/__tests__/*.js --watch", "test:describeWithDOMOnly": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMOnly-spec.js", "test:describeWithDOMSkip": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMSkip-spec.js", "test:all": "npm run react:13 && npm test && npm run test:describeWithDOMOnly && npm run test:describeWithDOMSkip && npm run react:14 && npm test && npm run test:describeWithDOMOnly && npm run test:describeWithDOMSkip", diff --git a/src/ReactWrapper.js b/src/ReactWrapper.js index 071edc814..2e6868cf8 100644 --- a/src/ReactWrapper.js +++ b/src/ReactWrapper.js @@ -137,6 +137,38 @@ export default class ReactWrapper { return this; } + /** + * A method that unmounts the component. This can be used to simulate a component going through + * and unmount/mount lifecycle. + * + * @returns {ReactWrapper} + */ + unmount() { + if (this.root !== this) { + throw new Error('ReactWrapper::unmount() can only be called on the root'); + } + this.single(() => { + this.component.setState({ mount: false }); + }); + return this; + } + + /** + * A method that re-mounts the component. This can be used to simulate a component going through + * an unmount/mount lifecycle. + * + * @returns {ReactWrapper} + */ + mount() { + if (this.root !== this) { + throw new Error('ReactWrapper::mount() can only be called on the root'); + } + this.single(() => { + this.component.setState({ mount: true }); + }); + return this; + } + /** * A method that sets the props of the root component, and re-renders. Useful for when you are * wanting to test how the component behaves over time with changing props. Calling this, for diff --git a/src/ReactWrapperComponent.jsx b/src/ReactWrapperComponent.jsx index 590689eb6..520f5201d 100644 --- a/src/ReactWrapperComponent.jsx +++ b/src/ReactWrapperComponent.jsx @@ -26,6 +26,7 @@ export default function createWrapperComponent(node, options = {}) { getInitialState() { return { + mount: true, props: this.props.props, context: this.props.context, }; @@ -62,8 +63,10 @@ export default function createWrapperComponent(node, options = {}) { render() { const { Component } = this.props; + const { mount, props } = this.state; + if (!mount) return null; return ( - + ); }, }; diff --git a/src/__tests__/ReactWrapper-spec.js b/src/__tests__/ReactWrapper-spec.js index 0ff8df3d4..af70bf000 100644 --- a/src/__tests__/ReactWrapper-spec.js +++ b/src/__tests__/ReactWrapper-spec.js @@ -1,3 +1,4 @@ +import { describeWithDOM, describeIf } from './_helpers'; import React from 'react'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -5,9 +6,7 @@ import { mount, render, ReactWrapper, - describeWithDOM, } from '../'; -import { describeIf } from './_helpers'; import { REACT013 } from '../version'; describeWithDOM('mount', () => { @@ -490,6 +489,68 @@ describeWithDOM('mount', () => { }); }); + + describe('.mount()', () => { + it('should call componentWillUnmount()', () => { + const willMount = sinon.spy(); + const didMount = sinon.spy(); + const willUnmount = sinon.spy(); + + class Foo extends React.Component { + constructor(props) { + super(props); + this.componentWillUnmount = willUnmount; + this.componentWillMount = willMount; + this.componentDidMount = didMount; + } + render() { + return ( +
+ {this.props.id} +
+ ); + } + } + const wrapper = mount(); + expect(willMount.callCount).to.equal(1); + expect(didMount.callCount).to.equal(1); + expect(willUnmount.callCount).to.equal(0); + wrapper.unmount(); + expect(willMount.callCount).to.equal(1); + expect(didMount.callCount).to.equal(1); + expect(willUnmount.callCount).to.equal(1); + wrapper.mount(); + expect(willMount.callCount).to.equal(2); + expect(didMount.callCount).to.equal(2); + expect(willUnmount.callCount).to.equal(1); + }); + }); + + describe('.unmount()', () => { + it('should call componentWillUnmount()', () => { + const spy = sinon.spy(); + + class Foo extends React.Component { + constructor(props) { + super(props); + this.componentWillUnmount = spy; + } + render() { + return ( +
+ {this.props.id} +
+ ); + } + } + const wrapper = mount(); + expect(spy.calledOnce).to.equal(false); + wrapper.unmount(); + expect(spy.calledOnce).to.equal(true); + }); + + }); + describe('.simulate(eventName, data)', () => { it('should simulate events', () => { diff --git a/src/__tests__/Utils-spec.js b/src/__tests__/Utils-spec.js index 229829b92..06f45ea66 100644 --- a/src/__tests__/Utils-spec.js +++ b/src/__tests__/Utils-spec.js @@ -1,4 +1,5 @@ -import React from 'react/addons'; +import { describeWithDOM } from './_helpers.js'; +import React from 'react'; import { expect } from 'chai'; import sinon from 'sinon'; import { @@ -12,10 +13,7 @@ import { selectorType, mapNativeEventNames, } from '../Utils'; -import { - describeWithDOM, - mount, -} from '../'; +import { mount } from '../'; describe('Utils', () => { diff --git a/src/__tests__/_helpers.js b/src/__tests__/_helpers.js index 91d619dcb..12f00361b 100644 --- a/src/__tests__/_helpers.js +++ b/src/__tests__/_helpers.js @@ -1,3 +1,14 @@ +export function describeWithDOM(a, b) { + describe('(uses jsdom)', () => { + if (global.document) { + describe(a, b); + } else { + // if jsdom isn't available, skip every test in this describe context + describe.skip(a, b); + } + }); +} + /** * Simple wrapper around mocha describe which allows a boolean to be passed in first which * determines whether or not the test will be run diff --git a/withDom.js b/withDom.js new file mode 100644 index 000000000..19b49d36b --- /dev/null +++ b/withDom.js @@ -0,0 +1,22 @@ +if (!global.document) { + try { + const jsdom = require('jsdom').jsdom; // could throw + + const exposedProperties = ['window', 'navigator', 'document']; + + global.document = jsdom(''); + global.window = document.defaultView; + Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } + }); + + global.navigator = { + userAgent: 'node.js', + }; + } catch (e) { + // jsdom is not supported... + } +}