Skip to content

Commit

Permalink
[New] mount: add renderProp
Browse files Browse the repository at this point in the history
  • Loading branch information
ljharb committed Nov 5, 2018
1 parent 7319312 commit 4512f08
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 0 deletions.
85 changes: 85 additions & 0 deletions 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<Any>`):

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 (
<div
style={{ height: '100%' }}
onMouseMove={(event) => {
this.setState({
x: event.clientX,
y: event.clientY,
});
}}
>
{render(this.state)}
</div>
);
}
}

Mouse.propTypes = {
render: PropTypes.func.isRequired,
};
```

```jsx
const App = () => (
<div style={{ height: '100%' }}>
<Mouse
render={(x = 0, y = 0) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)}
/>
</div>
);
```

##### Testing with no arguments

```jsx
const wrapper = mount(<App />)
.find(Mouse)
.renderProp('render');

expect(wrapper.equals(<h1>The mouse position is 0, 0</h1>)).to.equal(true);
```

##### Testing with multiple arguments

```jsx
const wrapper = mount(<App />)
.find(Mouse)
.renderProp('render', [10, 20]);

expect(wrapper.equals(<h1>The mouse position is 10, 20</h1>)).to.equal(true);
```
3 changes: 3 additions & 0 deletions docs/api/mount.md
Expand Up @@ -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.

Expand Down
62 changes: 62 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Expand Up @@ -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 <div className="in-foo" />;
}
}
class Bar extends React.Component {
render() {
const { render: r } = this.props;
return <div className="in-bar">{r()}</div>;
}
}

const wrapperA = mount(<div><Bar render={() => <div><Foo /></div>} /></div>);
const renderPropWrapperA = wrapperA.find(Bar).renderProp('render');
expect(renderPropWrapperA.find(Foo)).to.have.lengthOf(1);

const wrapperB = mount(<div><Bar render={() => <Foo />} /></div>);
const renderPropWrapperB = wrapperB.find(Bar).renderProp('render');
expect(renderPropWrapperB.find(Foo)).to.have.lengthOf(1);

const stub = sinon.stub().returns(<div />);
const wrapperC = mount(<div><Bar render={stub} /></div>);
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 <div>{children}</div>;
}
}

const wrapper = mount(<Div />).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 <div className="in-foo" />;
}
}
class Bar extends React.Component {
render() {
const { render: r } = this.props;
return <div className="in-bar">{r()}</div>;
}
}

const wrapper = mount(<div><Bar render={() => <div><Foo /></div>} /></div>);
expect(() => wrapper.find(Bar).renderProp('render')).to.throw(RangeError);
});
});

describe('lifecycle methods', () => {
describeIf(is('>= 16.3'), 'getDerivedStateFromProps', () => {
let spy;
Expand Down
35 changes: 35 additions & 0 deletions 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,
Expand Down Expand Up @@ -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.
*
Expand Down

0 comments on commit 4512f08

Please sign in to comment.