Skip to content

Commit

Permalink
Add mount() API for wrapping the root in a component
Browse files Browse the repository at this point in the history
  • Loading branch information
minznerjosh committed Mar 13, 2019
1 parent bf2c64f commit 7aaa70b
Show file tree
Hide file tree
Showing 10 changed files with 724 additions and 4 deletions.
2 changes: 2 additions & 0 deletions SUMMARY.md
Expand Up @@ -42,6 +42,7 @@
* [first()](/docs/api/ShallowWrapper/first.md)
* [forEach(fn)](/docs/api/ShallowWrapper/forEach.md)
* [get(index)](/docs/api/ShallowWrapper/get.md)
* [getWrappingComponent()](/docs/api/ShallowWrapper/getWrappingComponent.md)
* [getElement(index)](/docs/api/ShallowWrapper/getElement.md)
* [getElements(index)](/docs/api/ShallowWrapper/getElements.md)
* [hasClass(className)](/docs/api/ShallowWrapper/hasClass.md)
Expand Down Expand Up @@ -104,6 +105,7 @@
* [forEach(fn)](/docs/api/ReactWrapper/forEach.md)
* [get(index)](/docs/api/ReactWrapper/get.md)
* [getDOMNode()](/docs/api/ReactWrapper/getDOMNode.md)
* [getWrappingComponent()](/docs/api/ReactWrapper/getWrappingComponent.md)
* [hasClass(className)](/docs/api/ReactWrapper/hasClass.md)
* [hostNodes()](/docs/api/ReactWrapper/hostNodes.md)
* [html()](/docs/api/ReactWrapper/html.md)
Expand Down
45 changes: 45 additions & 0 deletions docs/api/ReactWrapper/getWrappingComponent.md
@@ -0,0 +1,45 @@
# `.getWrappingComponent() => ReactWrapper`

If a `wrappingComponent` was passed in `options`, this methods returns a `ReactWrapper` around the rendered `wrappingComponent`. This `ReactWrapper` can be used to update the `wrappingComponent`'s props, state, etc.


#### Returns

`ReactWrapper`: A `ReactWrapper` around the rendered `wrappingComponent`



#### Examples

```jsx
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import store from './my/app/store';
import mockStore from './my/app/mockStore';

function MyProvider(props) {
const { children, customStore } = props;

return (
<Provider store={customStore || store}>
<Router>
{children}
</Router>
</Provider>
);
}
MyProvider.propTypes = {
children: PropTypes.node,
customStore: PropTypes.shape({}),
};
MyProvider.defaultProps = {
children: null,
customStore: null,
};

const wrapper = mount(<MyComponent />, {
wrappingComponent: MyProvider,
});
const provider = wrapper.getWrappingComponent();
provider.setProps({ customStore: mockStore });
```
45 changes: 45 additions & 0 deletions docs/api/ShallowWrapper/getWrappingComponent.md
@@ -0,0 +1,45 @@
# `.getWrappingComponent() => ShallowWrapper`

If a `wrappingComponent` was passed in `options`, this methods returns a `ShallowWrapper` around the rendered `wrappingComponent`. This `ShallowWrapper` can be used to update the `wrappingComponent`'s props, state, etc.


#### Returns

`ShallowWrapper`: A `ShallowWrapper` around the rendered `wrappingComponent`



#### Examples

```jsx
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import store from './my/app/store';
import mockStore from './my/app/mockStore';

function MyProvider(props) {
const { children, customStore } = props;

return (
<Provider store={customStore || store}>
<Router>
{children}
</Router>
</Provider>
);
}
MyProvider.propTypes = {
children: PropTypes.node,
customStore: PropTypes.shape({}),
};
MyProvider.defaultProps = {
children: null,
customStore: null,
};

const wrapper = shallow(<MyComponent />, {
wrappingComponent: MyProvider,
});
const provider = wrapper.getWrappingComponent();
provider.setProps({ customStore: mockStore });
```
5 changes: 5 additions & 0 deletions docs/api/mount.md
Expand Up @@ -49,6 +49,8 @@ describe('<Foo />', () => {
- `options.context`: (`Object` [optional]): Context to be passed into the component
- `options.attachTo`: (`DOMElement` [optional]): DOM Element to attach the component to.
- `options.childContextTypes`: (`Object` [optional]): Merged contextTypes for all children of the wrapper.
- `options.wrappingComponent`: (`ComponentType` [optional]): A component that will render as a parent of the `node`. It can be used to provide context to the `node`, among other things. See the [`getWrappingComponent()` docs](ReactWrapper/getWrappingComponent.md) for an example. **Note**: `wrappingComponent` _must_ render its children.
- `options.wrappingComponentProps`: (`Object` [optional]): Initial props to pass to the `wrappingComponent` if it is specified.

#### Returns

Expand Down Expand Up @@ -177,6 +179,9 @@ Manually sets context of the root component.
#### [`.instance() => ReactComponent|DOMComponent`](ReactWrapper/instance.md)
Returns the wrapper's underlying instance.

#### [`.getWrappingComponent() => ReactWrapper`](ReactWrapper/getWrappingComponent.md)
Returns a wrapper representing the `wrappingComponent`, if one was passed.

#### [`.unmount() => ReactWrapper`](ReactWrapper/unmount.md)
A method that un-mounts the component.

Expand Down
5 changes: 5 additions & 0 deletions docs/api/shallow.md
Expand Up @@ -50,6 +50,8 @@ describe('<MyComponent />', () => {
- `options.disableLifecycleMethods`: (`Boolean` [optional]): If set to true, `componentDidMount`
is not called on the component, and `componentDidUpdate` is not called after
[`setProps`](ShallowWrapper/setProps.md) and [`setContext`](ShallowWrapper/setContext.md). Default to `false`.
- `options.wrappingComponent`: (`ComponentType` [optional]): A component that will render as a parent of the `node`. It can be used to provide context to the `node`, among other things. See the [`getWrappingComponent()` docs](ShallowWrapper/getWrappingComponent.md) for an example. **Note**: `wrappingComponent` _must_ render its children.
- `options.wrappingComponentProps`: (`Object` [optional]): Initial props to pass to the `wrappingComponent` if it is specified.

#### Returns

Expand Down Expand Up @@ -187,6 +189,9 @@ Manually sets props of the root component.
#### [`.setContext(context) => ShallowWrapper`](ShallowWrapper/setContext.md)
Manually sets context of the root component.

#### [`.getWrappingComponent() => ShallowWrapper`](ShallowWrapper/getWrappingComponent.md)
Returns a wrapper representing the `wrappingComponent`, if one was passed.

#### [`.instance() => ReactComponent`](ShallowWrapper/instance.md)
Returns the instance of the root component.

Expand Down
185 changes: 185 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Expand Up @@ -176,6 +176,191 @@ describeWithDOM('mount', () => {
expect(() => wrapper.state('key')).to.throw('ReactWrapper::state("key") requires that `state` not be `null` or `undefined`');
});

describeIf(is('>= 0.14'), 'wrappingComponent', () => {
const realCreateMountRenderer = getAdapter().createMountRenderer;
let wrapper;

class More extends React.Component {
render() {
return null;
}
}

class TestProvider extends React.Component {
getChildContext() {
const { value, renderMore } = this.props;

return {
testContext: value || 'Hello world!',
renderMore: renderMore || false,
};
}

render() {
const { children } = this.props;

return children;
}
}
TestProvider.childContextTypes = {
testContext: PropTypes.string,
renderMore: PropTypes.bool,
};

class MyWrappingComponent extends React.Component {
render() {
const { children, contextValue, renderMore } = this.props;

return (
<div>
<TestProvider value={contextValue} renderMore={renderMore}>{children}</TestProvider>
</div>
);
}
}

class MyComponent extends React.Component {
render() {
const { testContext, renderMore } = this.context;

return (
<div>
<div>Context says: {testContext}</div>
{renderMore && <More />}
</div>
);
}
}
MyComponent.contextTypes = TestProvider.childContextTypes;

beforeEach(() => {
wrapper = mount(<MyComponent />, {
wrappingComponent: MyWrappingComponent,
});
});

it('mounts the passed node as the root as per usual', () => {
expect(wrapper.type()).to.equal(MyComponent);
expect(wrapper.parent().exists()).to.equal(false);
expect(() => wrapper.setProps({ foo: 'bar' })).not.to.throw();
});

it('renders the root in the wrapping component', () => {
// Context will only be set properly if the root node is rendered as a descendent of
// the wrapping component.
expect(wrapper.text()).to.equal('Context says: Hello world!');
});

it('supports mounting the wrapping component with initial props', () => {
wrapper.unmount();
wrapper = mount(<MyComponent />, {
wrappingComponent: MyWrappingComponent,
wrappingComponentProps: { contextValue: 'I can be set!' },
});
expect(wrapper.text()).to.equal('Context says: I can be set!');
});

it('throws an error if the wrappingComponent does not render its children', () => {
class BadWrapper extends React.Component {
render() {
return <div />;
}
}
expect(() => mount(<MyComponent />, {
wrappingComponent: BadWrapper,
})).to.throw('`wrappingComponent` must render its children!');
});

describe('getWrappingComponent()', () => {
let wrappingComponent;

beforeEach(() => {
wrappingComponent = wrapper.getWrappingComponent();
});

it('gets a ReactWrapper for the wrappingComponent', () => {
expect(wrappingComponent.type()).to.equal(MyWrappingComponent);
expect(wrappingComponent.parent().exists()).to.equal(false);

wrappingComponent.setProps({ contextValue: 'this is a test.' });
expect(wrapper.text()).to.equal('Context says: this is a test.');
});

it('updates the wrapper when the wrappingComponent is updated', () => {
wrappingComponent.setProps({ renderMore: true });
expect(wrapper.find(More).exists()).to.equal(true);
});

it('updates the wrappingComponent when the root is updated', () => {
wrapper.unmount();
expect(wrappingComponent.exists()).to.equal(false);
});

it('handles the wrapper being unmounted', () => {
wrapper.unmount();
wrappingComponent.update();
expect(wrappingComponent.exists()).to.equal(false);
expect(() => wrappingComponent.setProps({})).to.throw('The wrapping component may not be updated if the root is unmounted.');
});

it('handles a partial prop update', () => {
wrappingComponent.setProps({ contextValue: 'hello' });
wrappingComponent.setProps({ foo: 'bar' });
expect(wrappingComponent.prop('foo')).to.equal('bar');
expect(wrappingComponent.prop('contextValue')).to.equal('hello');
});

it('cannot be called on the non-root', () => {
expect(() => wrapper.find('div').getWrappingComponent()).to.throw('ReactWrapper::getWrappingComponent() can only be called on the root');
});

it('cannot be called on itself', () => {
expect(() => wrappingComponent.getWrappingComponent()).to.throw('ReactWrapper::getWrappingComponent() can only be called on the root');
});

it('throws an error if `wrappingComponent` was not provided', () => {
wrapper.unmount();
wrapper = mount(<MyComponent />);
expect(() => wrapper.getWrappingComponent()).to.throw('ReactWrapper::getWrappingComponent() can only be called on a wrapper that was originally passed a `wrappingComponent` option');
});
});

wrap()
.withOverrides(() => getAdapter(), () => ({
RootFinder: undefined,
createMountRenderer: (...args) => {
const renderer = realCreateMountRenderer(...args);
delete renderer.getWrappingComponentRenderer;
renderer.getNode = () => null;
return renderer;
},
isCustomComponent: undefined,
}))
.describe('with an old adapter', () => {
it('renders fine when wrappingComponent is not passed', () => {
wrapper = mount(<MyComponent />);
});

it('throws an error if wrappingComponent is passed', () => {
expect(() => mount(<MyComponent />, {
wrappingComponent: MyWrappingComponent,
})).to.throw('your adapter does not support `wrappingComponent`. Try upgrading it!');
});
});
});

itIf(is('<=0.13'), 'throws an error if wrappingComponent is passed', () => {
class WrappingComponent extends React.Component {
render() {
const { children } = this.props;
return children;
}
}
expect(() => mount(<div />, {
wrappingComponent: WrappingComponent,
})).to.throw('your adapter does not support `wrappingComponent`. Try upgrading it!');
});

describeIf(is('>= 16.3'), 'uses the isValidElementType from the Adapter to validate the prop type of Component', () => {
const Foo = () => null;
const Bar = () => null;
Expand Down

0 comments on commit 7aaa70b

Please sign in to comment.