From 17e4904f23bbbec9dfbfbac2b66ec033b59f4887 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 20 Sep 2016 23:30:06 -0700 Subject: [PATCH 1/3] Utils: Add `isCustomComponentElement` --- src/Utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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) { From f8fb23cd0d74df2e62884d6869490dedd639de3e Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 20 Sep 2016 23:30:24 -0700 Subject: [PATCH 2/3] react-compat: add `isDOMComponentElement` --- src/react-compat.js | 5 +++++ 1 file changed, 5 insertions(+) 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, From c2414ca8f57b04a4d94bd8ef8869c852de5bfa66 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 20 Sep 2016 23:30:41 -0700 Subject: [PATCH 3/3] [New] `shallow`: add `.dive()` --- docs/api/ShallowWrapper/dive.md | 52 ++++++++++++++++++++++++ docs/api/shallow.md | 3 ++ src/ShallowWrapper.js | 24 ++++++++++- test/ShallowWrapper-spec.jsx | 70 ++++++++++++++++++++++++++++++++- 4 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 docs/api/ShallowWrapper/dive.md 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/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); + }); + }); });