Skip to content

Commit

Permalink
[New] Add ability to mount()/shallow() a node inside a wrapping compo…
Browse files Browse the repository at this point in the history
…nent (#1960)

Merge pull request #1960 from trialspark/josh__mount-new-context

Related to #1958.
  • Loading branch information
ljharb committed Apr 2, 2019
2 parents f652724 + cc3db79 commit a5edaf1
Show file tree
Hide file tree
Showing 26 changed files with 1,475 additions and 29 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
4 changes: 4 additions & 0 deletions packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js
Expand Up @@ -277,6 +277,10 @@ class ReactThirteenAdapter extends EnzymeAdapter {
return typeof object === 'string' || typeof object === 'function';
}

isCustomComponent(component) {
return typeof component === 'function';
}

createElement(...args) {
return React.createElement(...args);
}
Expand Down
34 changes: 33 additions & 1 deletion packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js
Expand Up @@ -19,6 +19,10 @@ import {
propsWithKeysAndRef,
ensureKeyOrUndefined,
wrap,
RootFinder,
getNodeFromRootFinder,
wrapWithWrappingComponent,
getWrappingComponentMountRenderer,
} from 'enzyme-adapter-utils';

function typeToNodeType(type) {
Expand Down Expand Up @@ -103,6 +107,7 @@ class ReactFourteenAdapter extends EnzymeAdapter {
const { type, props, ref } = el;
const wrapperProps = {
Component: type,
wrappingComponentProps: options.wrappingComponentProps,
props,
context,
...(ref && { ref }),
Expand All @@ -122,7 +127,14 @@ class ReactFourteenAdapter extends EnzymeAdapter {
instance = null;
},
getNode() {
return instance ? instanceToTree(instance._reactInternalInstance).rendered : null;
if (!instance) {
return null;
}
return getNodeFromRootFinder(
adapter.isCustomComponent,
instanceToTree(instance._reactInternalInstance),
options,
);
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event);
Expand All @@ -136,6 +148,15 @@ class ReactFourteenAdapter extends EnzymeAdapter {
batchedUpdates(fn) {
return ReactDOM.unstable_batchedUpdates(fn);
},
getWrappingComponentRenderer() {
return {
...this,
...getWrappingComponentMountRenderer({
toTree: inst => instanceToTree(inst._reactInternalInstance),
getMountWrapperInstance: () => instance,
}),
};
},
};
}

Expand Down Expand Up @@ -252,9 +273,20 @@ class ReactFourteenAdapter extends EnzymeAdapter {
return isValidElementType(object);
}

isCustomComponent(component) {
return typeof component === 'function';
}

createElement(...args) {
return React.createElement(...args);
}

wrapWithWrappingComponent(node, options) {
return {
RootFinder,
node: wrapWithWrappingComponent(React.createElement, node, options),
};
}
}

module.exports = ReactFourteenAdapter;
Expand Up @@ -19,6 +19,10 @@ import {
propsWithKeysAndRef,
ensureKeyOrUndefined,
wrap,
RootFinder,
getNodeFromRootFinder,
wrapWithWrappingComponent,
getWrappingComponentMountRenderer,
} from 'enzyme-adapter-utils';
import ifReact from 'enzyme-adapter-react-helper/build/ifReact';

Expand Down Expand Up @@ -138,6 +142,7 @@ class ReactFifteenFourAdapter extends EnzymeAdapter {
const { type, props, ref } = el;
const wrapperProps = {
Component: type,
wrappingComponentProps: options.wrappingComponentProps,
props,
context,
...(ref && { ref }),
Expand All @@ -157,7 +162,14 @@ class ReactFifteenFourAdapter extends EnzymeAdapter {
instance = null;
},
getNode() {
return instance ? instanceToTree(instance._reactInternalInstance).rendered : null;
if (!instance) {
return null;
}
return getNodeFromRootFinder(
adapter.isCustomComponent,
instanceToTree(instance._reactInternalInstance),
options,
);
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event, eventOptions);
Expand All @@ -171,6 +183,15 @@ class ReactFifteenFourAdapter extends EnzymeAdapter {
batchedUpdates(fn) {
return ReactDOM.unstable_batchedUpdates(fn);
},
getWrappingComponentRenderer() {
return {
...this,
...getWrappingComponentMountRenderer({
toTree: inst => instanceToTree(inst._reactInternalInstance),
getMountWrapperInstance: () => instance,
}),
};
},
};
}

Expand Down Expand Up @@ -287,6 +308,10 @@ class ReactFifteenFourAdapter extends EnzymeAdapter {
return isValidElementType(object);
}

isCustomComponent(component) {
return typeof component === 'function';
}

createElement(...args) {
return React.createElement(...args);
}
Expand All @@ -300,6 +325,13 @@ class ReactFifteenFourAdapter extends EnzymeAdapter {
);
invoke();
}

wrapWithWrappingComponent(node, options) {
return {
RootFinder,
node: wrapWithWrappingComponent(React.createElement, node, options),
};
}
}

module.exports = ReactFifteenFourAdapter;
34 changes: 33 additions & 1 deletion packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js
Expand Up @@ -21,6 +21,10 @@ import {
propsWithKeysAndRef,
ensureKeyOrUndefined,
wrap,
RootFinder,
getNodeFromRootFinder,
wrapWithWrappingComponent,
getWrappingComponentMountRenderer,
} from 'enzyme-adapter-utils';

function compositeTypeToNodeType(type) {
Expand Down Expand Up @@ -138,6 +142,7 @@ class ReactFifteenAdapter extends EnzymeAdapter {
const { type, props, ref } = el;
const wrapperProps = {
Component: type,
wrappingComponentProps: options.wrappingComponentProps,
props,
context,
...(ref && { ref }),
Expand All @@ -157,7 +162,14 @@ class ReactFifteenAdapter extends EnzymeAdapter {
instance = null;
},
getNode() {
return instance ? instanceToTree(instance._reactInternalInstance).rendered : null;
if (!instance) {
return null;
}
return getNodeFromRootFinder(
adapter.isCustomComponent,
instanceToTree(instance._reactInternalInstance),
options,
);
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event, eventOptions);
Expand All @@ -171,6 +183,15 @@ class ReactFifteenAdapter extends EnzymeAdapter {
batchedUpdates(fn) {
return ReactDOM.unstable_batchedUpdates(fn);
},
getWrappingComponentRenderer() {
return {
...this,
...getWrappingComponentMountRenderer({
toTree: inst => instanceToTree(inst._reactInternalInstance),
getMountWrapperInstance: () => instance,
}),
};
},
};
}

Expand Down Expand Up @@ -287,6 +308,10 @@ class ReactFifteenAdapter extends EnzymeAdapter {
return isValidElementType(object);
}

isCustomComponent(component) {
return typeof component === 'function';
}

createElement(...args) {
return React.createElement(...args);
}
Expand All @@ -295,6 +320,13 @@ class ReactFifteenAdapter extends EnzymeAdapter {
// React in >= 15.4, and < 16 pass undefined to a setState callback
callback.call(instance, undefined);
}

wrapWithWrappingComponent(node, options) {
return {
RootFinder,
node: wrapWithWrappingComponent(React.createElement, node, options),
};
}
}

module.exports = ReactFifteenAdapter;

0 comments on commit a5edaf1

Please sign in to comment.