diff --git a/docs/api/ReactWrapper/wrapProp.md b/docs/api/ReactWrapper/wrapProp.md new file mode 100644 index 000000000..7517de7fa --- /dev/null +++ b/docs/api/ReactWrapper/wrapProp.md @@ -0,0 +1,60 @@ +# `.wrapProp(propName[, options]) => ReactWrapper` + +Returns a new wrapper around the component provided to the original wrapper's prop `propName`.` + +#### Arguments + +1. `propName` (`String`): Name of the prop to be wrapped +1. `options` (`Object` [optional]): Will be passed to the renderer constructor. + Refer to the [`mount()` options](https://enzymejs.github.io/enzyme/docs/api/mount.html#arguments). + +This essentially does: + +```jsx +const Node = () => wrapper.prop('node'); +const node = mount(); +``` + +#### Returns + +`ReactWrapper`: A new wrapper that wraps the node from the provided prop. + +#### Examples + +##### Test Setup + +```jsx +class Inner extends React.Component { + render() { + return
; + } +} + +class Outer extends React.Component { + render() { + if (!this.props.renderNode) return
; + return this.props.node; + } +} + +class Container extends React.Component { + render() { + /* + * Just as an example, can render or not the provided prop. + * Independent of what it does, you want to test the component given to node. + */ + return } />; + } +} +``` + +##### Testing with no arguments + +```jsx +const wrapper = mount() + .find(Outer) + .wrapProp('node'); + +expect(wrapper.find('div').equals(
)).to.equal(true); +expect(wrapper.html()).to.equal('
'); +``` diff --git a/docs/api/ShallowWrapper/wrapProp.md b/docs/api/ShallowWrapper/wrapProp.md new file mode 100644 index 000000000..748beca58 --- /dev/null +++ b/docs/api/ShallowWrapper/wrapProp.md @@ -0,0 +1,59 @@ +# `.wrapProp(propName[, options]) => ShallowWrapper` + +Returns a new wrapper around the component provided to the original wrapper's prop `propName`.` + +#### Arguments + +1. `propName` (`String`): Name of the prop to be wrapped +1. `options` (`Object` [optional]): Will be passed to the renderer constructor. + Refer to the [`shallow()` options](https://enzymejs.github.io/enzyme/docs/api/shallow.html#arguments). + +This essentially does: + +```jsx +const Node = () => wrapper.prop('node'); +const node = shallow(); +``` + +#### Returns + +`ShallowWrapper`: A new wrapper that wraps the node from the provided prop. + +#### Examples + +##### Test Setup + +```jsx +class Inner extends React.Component { + render() { + return
; + } +} + +class Outer extends React.Component { + render() { + if (!this.props.renderNode) return
; + return this.props.node; + } +} + +class Container extends React.Component { + render() { + /* + * Just as an example, can render or not the provided prop. + * Independent of what it does, you want to test the component given to node. + */ + return } />; + } +} +``` + +##### Testing with no arguments + +```jsx +const wrapper = shallow() + .find(Outer) + .wrapProp('node'); + +expect(wrapper.equals(
)).to.equal(true); +``` diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index fb246711b..52930e5a6 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -1077,6 +1077,7 @@ describeWithDOM('mount', () => { 'text', 'unmount', 'wrap', + 'wrapProp', ); describeHooks( { Wrap, Wrapper }, diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 7c894cf75..bfa35e55c 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -1285,6 +1285,7 @@ describe('shallow', () => { 'text', 'unmount', 'wrap', + 'wrapProp', ); describeHooks( { Wrap, Wrapper }, diff --git a/packages/enzyme-test-suite/test/shared/methods/wrapProp.jsx b/packages/enzyme-test-suite/test/shared/methods/wrapProp.jsx new file mode 100644 index 000000000..e12631861 --- /dev/null +++ b/packages/enzyme-test-suite/test/shared/methods/wrapProp.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { expect } from 'chai'; +import wrap from 'mocha-wrap'; + +import getAdapter from 'enzyme/build/getAdapter'; + +export default function describeWrapProp({ + Wrap, + WrapRendered, + WrapperName, + isShallow, +}) { + wrap() + .withConsoleThrows() + .describe('.wrapProp()', () => { + class Inner extends React.Component { + render() { + return
; + } + } + class Outer extends React.Component { + render() { + return
; + } + } + class Container extends React.Component { + render() { + const node = this.props.replace || ; + return ; + } + } + + it('returns a wrapper around the node provided by the given prop', () => { + const wrapper = Wrap(); + const wrappedPropWrapper = wrapper.find(Outer).wrapProp('node'); + expect(wrappedPropWrapper.find('div').equals(
)).to.equal(true); + if (isShallow) expect(wrappedPropWrapper.equals(
)).to.equal(true); + }); + + it('throws on a non-string prop name', () => { + const wrapper = Wrap(); + expect(() => wrapper.find(Outer).wrapProp([])).to.throw( + TypeError, + `${WrapperName}::wrapProp(): \`propName\` must be a string`, + ); + }); + + it('throws on a missing prop', () => { + const wrapper = Wrap(); + expect(() => wrapper.find(Outer).wrapProp('missing')).to.throw( + Error, + `${WrapperName}::wrapProp(): no prop called "missing" found`, + ); + }); + + it('throws on an invalid element prop value', () => { + const wrapper = Wrap(
} />); + expect(() => wrapper.find(Outer).wrapProp('node')).to.throw( + TypeError, + `${WrapperName}::wrapProp(): prop "node" does not contain a valid element`, + ); + }); + + wrap() + .withOverride(() => getAdapter(), 'wrap', () => undefined) + .it('throws with a react adapter that lacks a `.wrap`', () => { + const wrapper = Wrap(); + expect(() => wrapper.find(Outer).wrapProp('foo')).to.throw(RangeError); + }); + }); +} diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js index 856df6609..0dbea795a 100644 --- a/packages/enzyme/src/ReactWrapper.js +++ b/packages/enzyme/src/ReactWrapper.js @@ -886,6 +886,36 @@ class ReactWrapper { }); } + /** + * Returns a new `ReactWrapper` around the node provided to the prop + * + * @param {String} propName + * @param {Object} options + * @returns {ReactWrapper} + */ + wrapProp(propName, options) { + const adapter = getAdapter(this[OPTIONS]); + if (typeof adapter.wrap !== 'function') { + throw new RangeError('your adapter does not support `wrap`. Try upgrading it!'); + } + + return this.single('wrapProp', () => { + if (typeof propName !== 'string') { + throw new TypeError('ReactWrapper::wrapProp(): `propName` must be a string'); + } + const props = this.props(); + if (!has(props, propName)) { + throw new Error(`ReactWrapper::wrapProp(): no prop called "${propName}" found`); + } + const node = props[propName]; + if (!adapter.isValidElement(node)) { + throw new TypeError(`ReactWrapper::wrapProp(): prop "${propName}" does not contain a valid element`); + } + + return new ReactWrapper(node, null, options); + }); + } + /** * Returns the key assigned to the current node. * @@ -1010,7 +1040,7 @@ class ReactWrapper { * * @param {Number} begin * @param {Number} end - * @returns {ShallowWrapper} + * @returns {ReactWrapper} */ slice(begin, end) { return this.wrap(this.getNodesInternal().slice(begin, end)); diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index f9821339b..c361ae77b 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -706,7 +706,7 @@ class ShallowWrapper { throw new Error('ShallowWrapper::setProps() can only be called on the root'); } if (arguments.length > 1 && typeof callback !== 'function') { - throw new TypeError('ReactWrapper::setProps() expects a function as its second argument'); + throw new TypeError('ShallowWrapper::setProps() expects a function as its second argument'); } this.rerender(props); if (callback) { @@ -736,7 +736,7 @@ class ShallowWrapper { throw new Error('ShallowWrapper::setState() can only be called on class components'); } if (arguments.length > 1 && typeof callback !== 'function') { - throw new TypeError('ReactWrapper::setState() expects a function as its second argument'); + throw new TypeError('ShallowWrapper::setState() expects a function as its second argument'); } this.single('setState', () => { @@ -1313,7 +1313,7 @@ class ShallowWrapper { /** * Used to invoke a function prop. - * Will invoke an function prop and return its value. + * Will invoke a function prop and return its value. * * @param {String} propName * @returns {Any} @@ -1368,6 +1368,36 @@ class ShallowWrapper { }); } + /** + * Returns a new `ShallowWrapper` around the node provided to the prop + * + * @param {String} propName + * @param {Object} options + * @returns {ShallowWrapper} + */ + wrapProp(propName, options) { + const adapter = getAdapter(this[OPTIONS]); + if (typeof adapter.wrap !== 'function') { + throw new RangeError('your adapter does not support `wrap`. Try upgrading it!'); + } + + return this.single('wrapProp', () => { + if (typeof propName !== 'string') { + throw new TypeError('ShallowWrapper::wrapProp(): `propName` must be a string'); + } + const props = this.props(); + if (!has(props, propName)) { + throw new Error(`ShallowWrapper::wrapProp(): no prop called "${propName}" found`); + } + const node = props[propName]; + if (!adapter.isValidElement(node)) { + throw new TypeError(`ShallowWrapper::wrapProp(): prop "${propName}" does not contain a valid element`); + } + + return new ShallowWrapper(node, null, options); + }); + } + /** * Returns the key assigned to the current node. *