diff --git a/docs/api/ShallowWrapper/dive.md b/docs/api/ShallowWrapper/dive.md
new file mode 100644
index 000000000..83c7d77fa
--- /dev/null
+++ b/docs/api/ShallowWrapper/dive.md
@@ -0,0 +1,52 @@
+# `.dive([options]) => ShallowWrapper`
+
+Shallow render the one non-DOM child of the current wrapper, and return a wrapper around the result.
+
+NOTE: can only be called on wrapper of a single non-DOM component element node.
+
+
+#### Arguments
+
+1. `options` (`Object` [optional]):
+- `options.context`: (`Object` [optional]): Context to be passed into the component
+
+
+
+#### Returns
+
+`ShallowWrapper`: A new wrapper that wraps the current node after it's been shallow rendered.
+
+
+
+#### Examples
+
+```jsx
+class Bar extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+```
+
+```jsx
+class Foo extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+```
+
+```jsx
+const wrapper = shallow();
+expect(wrapper.find('.in-bar')).to.have.length(0);
+expect(wrapper.find(Bar)).to.have.length(1);
+expect(wrapper.dive().find('.in-bar')).to.have.length(1);
+```
diff --git a/docs/api/shallow.md b/docs/api/shallow.md
index f9468739d..2e31a3b31 100644
--- a/docs/api/shallow.md
+++ b/docs/api/shallow.md
@@ -207,3 +207,6 @@ Returns whether or not all of the nodes in the wrapper match the provided select
#### [`.everyWhere(predicate) => Boolean`](ShallowWrapper/everyWhere.md)
Returns whether or not all of the nodes in the wrapper pass the provided predicate function.
+
+#### [`.dive([options]) => ShallowWrapper`](ShallowWrapper/dive.md)
+Shallow render the one non-DOM child of the current wrapper, and return a wrapper around the result.
diff --git a/src/ShallowWrapper.js b/src/ShallowWrapper.js
index fe0284643..13e49d95d 100644
--- a/src/ShallowWrapper.js
+++ b/src/ShallowWrapper.js
@@ -15,6 +15,7 @@ import {
isReactElementAlike,
displayNameOfNode,
isFunctionalComponent,
+ isCustomComponentElement,
} from './Utils';
import {
debugNodes,
@@ -31,6 +32,7 @@ import {
createShallowRenderer,
renderToStaticMarkup,
batchedUpdates,
+ isDOMComponentElement,
} from './react-compat';
/**
@@ -698,7 +700,7 @@ export default class ShallowWrapper {
/**
* Returns the type of the current node of this wrapper. If it's a composite component, this will
- * be the component constructor. If it's native DOM node, it will be a string.
+ * be the component constructor. If it's a native DOM node, it will be a string.
*
* @returns {String|Function}
*/
@@ -959,4 +961,24 @@ export default class ShallowWrapper {
intercepter(this);
return this;
}
+
+ /**
+ * Primarily useful for HOCs (higher-order components), this method may only be
+ * run on a single, non-DOM node, and will return the node, shallow-rendered.
+ *
+ * @param options object
+ * @returns {ShallowWrapper}
+ */
+ dive(options) {
+ const name = 'dive';
+ return this.single(name, (n) => {
+ if (isDOMComponentElement(n)) {
+ throw new TypeError(`ShallowWrapper::${name}() can not be called on DOM components`);
+ }
+ if (!isCustomComponentElement(n)) {
+ throw new TypeError(`ShallowWrapper::${name}() can only be called on components`);
+ }
+ return new ShallowWrapper(n, null, options);
+ });
+ }
}
diff --git a/src/Utils.js b/src/Utils.js
index b6428d34b..8eb9b0e4e 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -22,7 +22,11 @@ export function internalInstance(inst) {
}
export function isFunctionalComponent(inst) {
- return inst && inst.constructor && inst.constructor.name === 'StatelessComponent';
+ return !!inst && !!inst.constructor && inst.constructor.name === 'StatelessComponent';
+}
+
+export function isCustomComponentElement(inst) {
+ return !!inst && React.isValidElement(inst) && typeof inst.type === 'function';
}
export function propsOfNode(node) {
diff --git a/src/react-compat.js b/src/react-compat.js
index a35e0dc63..56cbaae36 100644
--- a/src/react-compat.js
+++ b/src/react-compat.js
@@ -150,6 +150,10 @@ if (REACT013) {
};
}
+function isDOMComponentElement(inst) {
+ return React.isValidElement(inst) && typeof inst.type === 'string';
+}
+
const {
mockComponent,
isElement,
@@ -170,6 +174,7 @@ export {
isElement,
isElementOfType,
isDOMComponent,
+ isDOMComponentElement,
isCompositeComponent,
isCompositeComponentWithType,
isCompositeComponentElement,
diff --git a/test/ShallowWrapper-spec.jsx b/test/ShallowWrapper-spec.jsx
index cc07faa68..2d8e4bbc8 100644
--- a/test/ShallowWrapper-spec.jsx
+++ b/test/ShallowWrapper-spec.jsx
@@ -69,7 +69,7 @@ describe('shallow', () => {
);
const context = { name: 'foo' };
- expect(() => shallow(, { context })).not.to.throw(Error);
+ expect(() => shallow(, { context })).not.to.throw();
});
it('is instrospectable through context API', () => {
@@ -3558,4 +3558,72 @@ describe('shallow', () => {
});
});
+ describe('.dive()', () => {
+ class RendersDOM extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ class RendersNull extends React.Component {
+ render() {
+ return null;
+ }
+ }
+ class RendersMultiple extends React.Component {
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+ class WrapsRendersDOM extends React.Component {
+ render() {
+ return ;
+ }
+ }
+ class DoubleWrapsRendersDOM extends React.Component {
+ render() {
+ return ;
+ }
+ }
+
+ it('throws on a DOM node', () => {
+ const wrapper = shallow();
+ expect(wrapper.is('div')).to.equal(true);
+
+ expect(() => { wrapper.dive(); }).to.throw(
+ TypeError,
+ 'ShallowWrapper::dive() can not be called on DOM components'
+ );
+ });
+
+ it('throws on a non-component', () => {
+ const wrapper = shallow();
+ expect(wrapper.type()).to.equal(null);
+
+ expect(() => { wrapper.dive(); }).to.throw(
+ TypeError,
+ 'ShallowWrapper::dive() can only be called on components'
+ );
+ });
+
+ it('throws on multiple children found', () => {
+ const wrapper = shallow().find('div').children();
+ expect(() => { wrapper.dive(); }).to.throw(
+ Error,
+ 'Method “dive” is only meant to be run on a single node. 2 found instead.'
+ );
+ });
+
+ it('dives + shallow-renders when there is one component child', () => {
+ const wrapper = shallow();
+ expect(wrapper.is(WrapsRendersDOM)).to.equal(true);
+
+ const underwater = wrapper.dive();
+ expect(underwater.is(RendersDOM)).to.equal(true);
+ });
+ });
});