Skip to content

Commit

Permalink
Merge pull request #155 from airbnb/lmr--mount-unmount
Browse files Browse the repository at this point in the history
Added mount/unmount APIs to ReactWrapper
  • Loading branch information
lelandrichardson committed Feb 2, 2016
2 parents e776630 + 1c4cff7 commit 2a0de29
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 11 deletions.
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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)
Expand Down
51 changes: 51 additions & 0 deletions docs/api/ReactWrapper/mount.md
Original file line number Diff line number Diff line change
@@ -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 (
<div className={this.props.id}>
{this.props.id}
</div>
);
}
}
const wrapper = mount(<Foo id="foo" />);
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)
47 changes: 47 additions & 0 deletions docs/api/ReactWrapper/unmount.md
Original file line number Diff line number Diff line change
@@ -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 (
<div className={this.props.id}>
{this.props.id}
</div>
);
}
}
const wrapper = mount(<Foo id="foo" />);
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)
6 changes: 6 additions & 0 deletions docs/api/mount.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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.

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 32 additions & 0 deletions src/ReactWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,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
Expand Down
5 changes: 4 additions & 1 deletion src/ReactWrapperComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function createWrapperComponent(node, options = {}) {

getInitialState() {
return {
mount: true,
props: this.props.props,
context: this.props.context,
};
Expand Down Expand Up @@ -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 (
<Component {...this.state.props} />
<Component {...props} />
);
},
};
Expand Down
65 changes: 63 additions & 2 deletions src/__tests__/ReactWrapper-spec.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { describeWithDOM, describeIf } from './_helpers';
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import {
mount,
render,
ReactWrapper,
describeWithDOM,
} from '../';
import { describeIf } from './_helpers';
import { REACT013 } from '../version';

describeWithDOM('mount', () => {
Expand Down Expand Up @@ -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 (
<div className={this.props.id}>
{this.props.id}
</div>
);
}
}
const wrapper = mount(<Foo id="foo" />);
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 (
<div className={this.props.id}>
{this.props.id}
</div>
);
}
}
const wrapper = mount(<Foo id="foo" />);
expect(spy.calledOnce).to.equal(false);
wrapper.unmount();
expect(spy.calledOnce).to.equal(true);
});

});

describe('.simulate(eventName, data)', () => {

it('should simulate events', () => {
Expand Down
8 changes: 3 additions & 5 deletions src/__tests__/Utils-spec.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -12,10 +13,7 @@ import {
selectorType,
mapNativeEventNames,
} from '../Utils';
import {
describeWithDOM,
mount,
} from '../';
import { mount } from '../';

describe('Utils', () => {

Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/_helpers.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 22 additions & 0 deletions withDom.js
Original file line number Diff line number Diff line change
@@ -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...
}
}

0 comments on commit 2a0de29

Please sign in to comment.