From 8494d0e7a9e6790e4be2272b4352f360eaac9830 Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Tue, 16 Oct 2018 21:32:54 +0200 Subject: [PATCH 1/3] [enzyme-adapter-react-*] [new] `wrap`: add `wrap` to all adapters [enzyme-adapter-utils] [new] add `wrapWithSimpleWrapper` --- .../src/ReactThirteenAdapter.js | 5 ++++ .../src/ReactFourteenAdapter.js | 5 ++++ .../src/ReactFifteenFourAdapter.js | 5 ++++ .../src/ReactFifteenAdapter.js | 5 ++++ .../src/ReactSixteenOneAdapter.js | 5 ++++ .../src/ReactSixteenTwoAdapter.js | 5 ++++ .../src/ReactSixteenThreeAdapter.js | 5 ++++ .../src/ReactSixteenAdapter.js | 5 ++++ packages/enzyme-adapter-utils/package.json | 3 +- packages/enzyme-adapter-utils/src/Utils.js | 3 +- .../src/wrapWithSimpleWrapper.jsx | 29 +++++++++++++++++++ .../enzyme-test-suite/test/Adapter-spec.jsx | 14 +++++++++ 12 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/enzyme-adapter-utils/src/wrapWithSimpleWrapper.jsx diff --git a/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js b/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js index 0c2dd7c88..6c9134d54 100644 --- a/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js +++ b/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js @@ -14,6 +14,7 @@ import { createMountWrapper, propsWithKeysAndRef, ensureKeyOrUndefined, + wrap, } from 'enzyme-adapter-utils'; import mapNativeEventNames from './ReactThirteenMapNativeEventNames'; import elementToTree from './ReactThirteenElementToTree'; @@ -242,6 +243,10 @@ class ReactThirteenAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js b/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js index 41a1625ac..9b1f1fb61 100644 --- a/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js +++ b/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js @@ -18,6 +18,7 @@ import { createMountWrapper, propsWithKeysAndRef, ensureKeyOrUndefined, + wrap, } from 'enzyme-adapter-utils'; function typeToNodeType(type) { @@ -214,6 +215,10 @@ class ReactFourteenAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js b/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js index 1515fce88..177642267 100644 --- a/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js +++ b/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js @@ -18,6 +18,7 @@ import { createMountWrapper, propsWithKeysAndRef, ensureKeyOrUndefined, + wrap, } from 'enzyme-adapter-utils'; import ifReact from 'enzyme-adapter-react-helper/build/ifReact'; @@ -249,6 +250,10 @@ class ReactFifteenFourAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js b/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js index 9dfa458a6..c68e73d84 100644 --- a/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js +++ b/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js @@ -20,6 +20,7 @@ import { createMountWrapper, propsWithKeysAndRef, ensureKeyOrUndefined, + wrap, } from 'enzyme-adapter-utils'; function compositeTypeToNodeType(type) { @@ -249,6 +250,10 @@ class ReactFifteenAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js index bec880000..d59974ca3 100644 --- a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js +++ b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js @@ -28,6 +28,7 @@ import { propsWithKeysAndRef, ensureKeyOrUndefined, simulateError, + wrap, } from 'enzyme-adapter-utils'; import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'; @@ -440,6 +441,10 @@ class ReactSixteenOneAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js index 8eedf3b41..4b64a859f 100644 --- a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js +++ b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js @@ -29,6 +29,7 @@ import { propsWithKeysAndRef, ensureKeyOrUndefined, simulateError, + wrap, } from 'enzyme-adapter-utils'; import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'; @@ -442,6 +443,10 @@ class ReactSixteenTwoAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js index a243eddde..e5fe1026f 100644 --- a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js +++ b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js @@ -36,6 +36,7 @@ import { propsWithKeysAndRef, ensureKeyOrUndefined, simulateError, + wrap, } from 'enzyme-adapter-utils'; import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'; @@ -422,6 +423,10 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index 73deffa71..2606e09c6 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -38,6 +38,7 @@ import { propsWithKeysAndRef, ensureKeyOrUndefined, simulateError, + wrap, } from 'enzyme-adapter-utils'; import findCurrentFiberUsingSlowPath from './findCurrentFiberUsingSlowPath'; import detectFiberTags from './detectFiberTags'; @@ -468,6 +469,10 @@ class ReactSixteenAdapter extends EnzymeAdapter { } } + wrap(element) { + return wrap(element); + } + // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. diff --git a/packages/enzyme-adapter-utils/package.json b/packages/enzyme-adapter-utils/package.json index 4eb344029..b70ad3217 100644 --- a/packages/enzyme-adapter-utils/package.json +++ b/packages/enzyme-adapter-utils/package.json @@ -36,7 +36,8 @@ "dependencies": { "function.prototype.name": "^1.1.0", "object.assign": "^4.1.0", - "prop-types": "^15.6.2" + "prop-types": "^15.6.2", + "semver": "^5.6.0" }, "peerDependencies": { "react": "0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0" diff --git a/packages/enzyme-adapter-utils/src/Utils.js b/packages/enzyme-adapter-utils/src/Utils.js index 8bfa18d11..9ce5eb763 100644 --- a/packages/enzyme-adapter-utils/src/Utils.js +++ b/packages/enzyme-adapter-utils/src/Utils.js @@ -1,8 +1,9 @@ import functionName from 'function.prototype.name'; import createMountWrapper from './createMountWrapper'; import createRenderWrapper from './createRenderWrapper'; +import wrap from './wrapWithSimpleWrapper'; -export { createMountWrapper, createRenderWrapper }; +export { createMountWrapper, createRenderWrapper, wrap }; export function mapNativeEventNames(event, { animation = false, // should be true for React 15+ diff --git a/packages/enzyme-adapter-utils/src/wrapWithSimpleWrapper.jsx b/packages/enzyme-adapter-utils/src/wrapWithSimpleWrapper.jsx new file mode 100644 index 000000000..3c6842c48 --- /dev/null +++ b/packages/enzyme-adapter-utils/src/wrapWithSimpleWrapper.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { intersects } from 'semver'; +import PropTypes from 'prop-types'; + +const propTypes = { + children: PropTypes.element.isRequired, +}; + +const Wrapper = (intersects('>= 0.14', React.version) + // eslint-disable-next-line prefer-arrow-callback + ? () => Object.assign(function SimpleSFCWrapper({ children }) { + return children; + }, { propTypes }) + : () => { + class SimpleClassWrapper extends React.Component { + render() { + const { children } = this.props; + return children; + } + } + SimpleClassWrapper.propTypes = propTypes; + return SimpleClassWrapper; + } +)(); + +export default function wrap(element) { + console.log(React.version, Wrapper); + return {element}; +} diff --git a/packages/enzyme-test-suite/test/Adapter-spec.jsx b/packages/enzyme-test-suite/test/Adapter-spec.jsx index 9994c683f..f5cd61975 100644 --- a/packages/enzyme-test-suite/test/Adapter-spec.jsx +++ b/packages/enzyme-test-suite/test/Adapter-spec.jsx @@ -1019,4 +1019,18 @@ describe('Adapter', () => { expect(adapter.isFragment(
)).to.equal(false); }); }); + + describe('.wrap()', () => { + it('returns a valid element', () => { + const element =
; + const wrapped = adapter.wrap(element); + expect(adapter.isValidElement(wrapped)).to.equal(true); + }); + + it('renders the children provided', () => { + const element =
; + const wrapped = adapter.wrap(element); + expect(wrapped.props).to.contain.keys({ children: element }); + }); + }); }); From ec66aaf563cb2fe91411de7791ecc6573eaa0cec Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Tue, 16 Oct 2018 20:41:16 +0200 Subject: [PATCH 2/3] [New] `shallow`: add renderProp --- docs/api/ShallowWrapper/renderProp.md | 85 +++++++++++++++++++ docs/api/shallow.md | 3 + .../test/ShallowWrapper-spec.jsx | 62 ++++++++++++++ packages/enzyme/src/ShallowWrapper.js | 35 ++++++++ 4 files changed, 185 insertions(+) create mode 100644 docs/api/ShallowWrapper/renderProp.md diff --git a/docs/api/ShallowWrapper/renderProp.md b/docs/api/ShallowWrapper/renderProp.md new file mode 100644 index 000000000..2e85ffc4e --- /dev/null +++ b/docs/api/ShallowWrapper/renderProp.md @@ -0,0 +1,85 @@ +# `.renderProp(propName, ...args) => ShallowWrapper` + +Calls the current wrapper's property with name `propName` and the `args` provided. +Returns the result in a new wrapper. + +NOTE: can only be called on wrapper of a single non-DOM component element node. + +#### Arguments + +1. `propName` (`String`): +1. `...args` (`Array`): + +This essentially calls `wrapper.prop(propName)(...args)`. + +#### Returns + +`ShallowWrapper`: A new wrapper that wraps the node returned from the render prop. + +#### Examples + +##### Test Setup + +```jsx +class Mouse extends React.Component { + constructor() { + super(); + this.state = { x: 0, y: 0 }; + } + + render() { + const { render } = this.props; + return ( +
{ + this.setState({ + x: event.clientX, + y: event.clientY, + }); + }} + > + {render(this.state)} +
+ ); + } +} + +Mouse.propTypes = { + render: PropTypes.func.isRequired, +}; +``` + +```jsx +const App = () => ( +
+ ( +

+ The mouse position is ({x}, {y}) +

+ )} + /> +
+); +``` + +##### Testing with no arguments + +```jsx +const wrapper = shallow() + .find(Mouse) + .renderProp('render'); + +expect(wrapper.equals(

The mouse position is 0, 0

)).to.equal(true); +``` + +##### Testing with multiple arguments + +```jsx +const wrapper = shallow() + .find(Mouse) + .renderProp('render', [10, 20]); + +expect(wrapper.equals(

The mouse position is 10, 20

)).to.equal(true); +``` diff --git a/docs/api/shallow.md b/docs/api/shallow.md index 388e0c6fb..8cdab0071 100644 --- a/docs/api/shallow.md +++ b/docs/api/shallow.md @@ -130,6 +130,9 @@ Shallow renders the current node and returns a shallow wrapper around it. #### [`.render() => CheerioWrapper`](ShallowWrapper/render.md) Returns a CheerioWrapper of the current node's subtree. +#### [`.renderProp(key) => ShallowWrapper`](ShallowWrapper/renderProp.md) +Returns a wrapper of the node rendered by the provided render prop. + #### [`.unmount() => ShallowWrapper`](ShallowWrapper/unmount.md) A method that un-mounts the component. diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 84a78810b..8e3aa0d80 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -4743,6 +4743,68 @@ describe('shallow', () => { }); }); + describe('.renderProp()', () => { + it('returns a wrapper around the node returned from the render prop', () => { + class Foo extends React.Component { + render() { + return
; + } + } + class Bar extends React.Component { + render() { + const { render: r } = this.props; + return
{r()}
; + } + } + + const wrapperA = shallow(
} />
); + const renderPropWrapperA = wrapperA.find(Bar).renderProp('render'); + expect(renderPropWrapperA.find(Foo)).to.have.lengthOf(1); + + const wrapperB = shallow(
} />
); + const renderPropWrapperB = wrapperB.find(Bar).renderProp('render'); + expect(renderPropWrapperB.find(Foo)).to.have.lengthOf(1); + + const stub = sinon.stub().returns(
); + const wrapperC = shallow(
); + stub.resetHistory(); + wrapperC.find(Bar).renderProp('render', 'one', 'two'); + expect(stub.args).to.deep.equal([['one', 'two']]); + }); + + it('throws on host elements', () => { + class Div extends React.Component { + render() { + const { children } = this.props; + return
{children}
; + } + } + + const wrapper = shallow(
); + expect(wrapper.is('div')).to.equal(true); + expect(() => wrapper.renderProp('foo')).to.throw(); + }); + + wrap() + .withOverride(() => getAdapter(), 'wrap', () => undefined) + .it('throws with a react adapter that lacks a `.wrap`', () => { + class Foo extends React.Component { + render() { + return
; + } + } + class Bar extends React.Component { + render() { + const { render: r } = this.props; + return
{r()}
; + } + } + + const wrapper = shallow(
} />
); + expect(() => wrapper.find(Bar).renderProp('render')).to.throw(RangeError); + }); + }); + describe('lifecycle methods', () => { describe('disableLifecycleMethods option', () => { describe('validation', () => { diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index 3a6456ef3..60d1c8c08 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -1,6 +1,7 @@ import flat from 'array.prototype.flat'; import isEqual from 'lodash.isequal'; import cheerio from 'cheerio'; +import has from 'has'; import { nodeEqual, @@ -1033,6 +1034,40 @@ class ShallowWrapper { return this.props()[propName]; } + /** + * Returns a wrapper of the node rendered by the provided render prop. + * + * @param {String} propName + * @returns {ShallowWrapper} + */ + renderProp(propName, ...args) { + 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('renderProp', (n) => { + if (n.nodeType === 'host') { + throw new TypeError('ShallowWrapper::renderProp() can only be called on custom components'); + } + if (typeof propName !== 'string') { + throw new TypeError('`propName` must be a string'); + } + const props = this.props(); + if (!has(props, propName)) { + throw new Error(`no prop called “${propName}“ found`); + } + const propValue = props[propName]; + if (typeof propValue !== 'function') { + throw new TypeError(`expected prop “${propName}“ to contain a function, but it holds “${typeof prop}“`); + } + + const element = propValue(...args); + const wrapped = adapter.wrap(element); + return this.wrap(wrapped, null, this[OPTIONS]); + }); + } + /** * Returns the key assigned to the current node. * From d72526177b1b94a7ed05a6f805e5bf73d778b446 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 4 Nov 2018 22:19:03 -0800 Subject: [PATCH 3/3] [New] `mount`: add `renderProp` --- docs/api/ReactWrapper/renderProp.md | 85 +++++++++++++++++++ docs/api/mount.md | 3 + .../test/ReactWrapper-spec.jsx | 62 ++++++++++++++ packages/enzyme/src/ReactWrapper.js | 35 ++++++++ 4 files changed, 185 insertions(+) create mode 100644 docs/api/ReactWrapper/renderProp.md diff --git a/docs/api/ReactWrapper/renderProp.md b/docs/api/ReactWrapper/renderProp.md new file mode 100644 index 000000000..0575d60dd --- /dev/null +++ b/docs/api/ReactWrapper/renderProp.md @@ -0,0 +1,85 @@ +# `.renderProp(propName, ...args) => ReactWrapper` + +Calls the current wrapper's property with name `propName` and the `args` provided. +Returns the result in a new wrapper. + +NOTE: can only be called on wrapper of a single non-DOM component element node. + +#### Arguments + +1. `propName` (`String`): +1. `...args` (`Array`): + +This essentially calls `wrapper.prop(propName)(...args)`. + +#### Returns + +`ReactWrapper`: A new wrapper that wraps the node returned from the render prop. + +#### Examples + +##### Test Setup + +```jsx +class Mouse extends React.Component { + constructor() { + super(); + this.state = { x: 0, y: 0 }; + } + + render() { + const { render } = this.props; + return ( +
{ + this.setState({ + x: event.clientX, + y: event.clientY, + }); + }} + > + {render(this.state)} +
+ ); + } +} + +Mouse.propTypes = { + render: PropTypes.func.isRequired, +}; +``` + +```jsx +const App = () => ( +
+ ( +

+ The mouse position is ({x}, {y}) +

+ )} + /> +
+); +``` + +##### Testing with no arguments + +```jsx +const wrapper = mount() + .find(Mouse) + .renderProp('render'); + +expect(wrapper.equals(

The mouse position is 0, 0

)).to.equal(true); +``` + +##### Testing with multiple arguments + +```jsx +const wrapper = mount() + .find(Mouse) + .renderProp('render', [10, 20]); + +expect(wrapper.equals(

The mouse position is 10, 20

)).to.equal(true); +``` diff --git a/docs/api/mount.md b/docs/api/mount.md index de0553497..fb4853e3a 100644 --- a/docs/api/mount.md +++ b/docs/api/mount.md @@ -120,6 +120,9 @@ Get a wrapper with the first ancestor of the current node to match the provided #### [`.render() => CheerioWrapper`](ReactWrapper/render.md) Returns a CheerioWrapper of the current node's subtree. +#### [`.renderProp(key) => ReactWrapper`](ReactWrapper/renderProp.md) +Returns a wrapper of the node rendered by the provided render prop. + #### [`.text() => String`](ReactWrapper/text.md) Returns a string representation of the text nodes in the current render tree. diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index 1be098277..9bed5d92b 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -4959,6 +4959,68 @@ describeWithDOM('mount', () => { }); }); + describe('.renderProp()', () => { + it('returns a wrapper around the node returned from the render prop', () => { + class Foo extends React.Component { + render() { + return
; + } + } + class Bar extends React.Component { + render() { + const { render: r } = this.props; + return
{r()}
; + } + } + + const wrapperA = mount(
} />
); + const renderPropWrapperA = wrapperA.find(Bar).renderProp('render'); + expect(renderPropWrapperA.find(Foo)).to.have.lengthOf(1); + + const wrapperB = mount(
} />
); + const renderPropWrapperB = wrapperB.find(Bar).renderProp('render'); + expect(renderPropWrapperB.find(Foo)).to.have.lengthOf(1); + + const stub = sinon.stub().returns(
); + const wrapperC = mount(
); + stub.resetHistory(); + wrapperC.find(Bar).renderProp('render', 'one', 'two'); + expect(stub.args).to.deep.equal([['one', 'two']]); + }); + + it('throws on host elements', () => { + class Div extends React.Component { + render() { + const { children } = this.props; + return
{children}
; + } + } + + const wrapper = mount(
).childAt(0); + expect(wrapper.is('div')).to.equal(true); + expect(() => wrapper.renderProp('foo')).to.throw(); + }); + + wrap() + .withOverride(() => getAdapter(), 'wrap', () => undefined) + .it('throws with a react adapter that lacks a `.wrap`', () => { + class Foo extends React.Component { + render() { + return
; + } + } + class Bar extends React.Component { + render() { + const { render: r } = this.props; + return
{r()}
; + } + } + + const wrapper = mount(
} />
); + expect(() => wrapper.find(Bar).renderProp('render')).to.throw(RangeError); + }); + }); + describe('lifecycle methods', () => { describeIf(is('>= 16.3'), 'getDerivedStateFromProps', () => { let spy; diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js index cb77964c2..dd5e67d59 100644 --- a/packages/enzyme/src/ReactWrapper.js +++ b/packages/enzyme/src/ReactWrapper.js @@ -1,5 +1,6 @@ import cheerio from 'cheerio'; import flat from 'array.prototype.flat'; +import has from 'has'; import { containsChildrenSubArray, @@ -779,6 +780,40 @@ class ReactWrapper { return this.props()[propName]; } + /** + * Returns a wrapper of the node rendered by the provided render prop. + * + * @param {String} propName + * @returns {ReactWrapper} + */ + renderProp(propName, ...args) { + 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('renderProp', (n) => { + if (n.nodeType === 'host') { + throw new TypeError('ReactWrapper::renderProp() can only be called on custom components'); + } + if (typeof propName !== 'string') { + throw new TypeError('`propName` must be a string'); + } + const props = this.props(); + if (!has(props, propName)) { + throw new Error(`no prop called “${propName}“ found`); + } + const propValue = props[propName]; + if (typeof propValue !== 'function') { + throw new TypeError(`expected prop “${propName}“ to contain a function, but it holds “${typeof prop}“`); + } + + const element = propValue(...args); + const wrapped = adapter.wrap(element); + return this.wrap(wrapped, null, this[OPTIONS]); + }); + } + /** * Returns the key assigned to the current node. *