diff --git a/SUMMARY.md b/SUMMARY.md index 3200c2826..86aa5d86e 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -49,6 +49,7 @@ * [hostNodes()](/docs/api/ShallowWrapper/hostNodes.md) * [html()](/docs/api/ShallowWrapper/html.md) * [instance()](/docs/api/ShallowWrapper/instance.md) + * [invoke(propName)](/docs/api/ShallowWrapper/invoke.md) * [is(selector)](/docs/api/ShallowWrapper/is.md) * [isEmpty()](/docs/api/ShallowWrapper/isEmpty.md) * [isEmptyRender()](/docs/api/ShallowWrapper/isEmptyRender.md) @@ -110,6 +111,7 @@ * [hostNodes()](/docs/api/ReactWrapper/hostNodes.md) * [html()](/docs/api/ReactWrapper/html.md) * [instance()](/docs/api/ReactWrapper/instance.md) + * [invoke(propName)](/docs/api/ReactWrapper/invoke.md) * [is(selector)](/docs/api/ReactWrapper/is.md) * [isEmpty()](/docs/api/ReactWrapper/isEmpty.md) * [isEmptyRender()](/docs/api/ReactWrapper/isEmptyRender.md) diff --git a/docs/api/ReactWrapper/invoke.md b/docs/api/ReactWrapper/invoke.md new file mode 100644 index 000000000..9ec867ffa --- /dev/null +++ b/docs/api/ReactWrapper/invoke.md @@ -0,0 +1,41 @@ +# `.invoke(propName)(...args) => Any` + +Invokes a function prop. + +#### Arguments + +1. `propName` (`String`): The function prop that is invoked +2. `...args` (`Any` [optional]): Arguments that is passed to the prop function + + + +#### Returns + +`Any`: Returns the value from the prop function + +#### Example + +```jsx +class Foo extends React.Component { + loadData() { + return fetch(); + } + + render() { + return ( +
+ +
+ ); + } +} +const wrapper = mount(); +wrapper.find('a').invoke('onClick')().then(() => { + // expect() +}); +``` diff --git a/docs/api/ShallowWrapper/invoke.md b/docs/api/ShallowWrapper/invoke.md new file mode 100644 index 000000000..2f9d32bde --- /dev/null +++ b/docs/api/ShallowWrapper/invoke.md @@ -0,0 +1,40 @@ +# `.invoke(invokePropName)(...args) => Any` + +Invokes a function prop. + +#### Arguments + +1. `propName` (`String`): The function prop that is invoked +2. `...args` (`Any` [optional]): Arguments that is passed to the prop function + +This essentially calls wrapper.prop(propName)(...args). + +#### Returns + +`Any`: Returns the value from the prop function + +#### Example + +```jsx +class Foo extends React.Component { + loadData() { + return fetch(); + } + + render() { + return ( +
+ +
+ ); + } +} +const wrapper = shallow(); +wrapper.find('a').invoke('onClick')().then(() => { + // expect() +}); diff --git a/docs/api/mount.md b/docs/api/mount.md index a5bdb704b..c3401fb44 100644 --- a/docs/api/mount.md +++ b/docs/api/mount.md @@ -161,6 +161,9 @@ Returns the props of the root component. #### [`.prop(key) => Any`](ReactWrapper/prop.md) Returns the named prop of the root component. +#### [`.invoke(propName)(...args) => Any`](ReactWrapper/invoke.md) +Invokes a prop function on the current node and returns the function's return value. + #### [`.key() => String`](ReactWrapper/key.md) Returns the key of the root component. diff --git a/docs/api/shallow.md b/docs/api/shallow.md index 77065a61a..0756bb06b 100644 --- a/docs/api/shallow.md +++ b/docs/api/shallow.md @@ -177,6 +177,9 @@ Returns the named prop of the current node. #### [`.key() => String`](ShallowWrapper/key.md) Returns the key of the current node. +#### [`.invoke(propName)(...args) => Any`](ShallowWrapper/invoke.md) +Invokes a prop function on the current node and returns the function's return value. + #### [`.simulate(event[, data]) => ShallowWrapper`](ShallowWrapper/simulate.md) Simulates an event on the current node. diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index e9fefa260..f61d2e591 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -1043,6 +1043,7 @@ describeWithDOM('mount', () => { 'hostNodes', 'html', 'instance', + 'invoke', 'is', 'isEmpty', 'isEmptyRender', diff --git a/packages/enzyme-test-suite/test/shared/methods/invoke.jsx b/packages/enzyme-test-suite/test/shared/methods/invoke.jsx new file mode 100644 index 000000000..844ae2217 --- /dev/null +++ b/packages/enzyme-test-suite/test/shared/methods/invoke.jsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon-sandbox'; + +export default function describeInvoke({ + Wrap, + WrapperName, +}) { + describe('.invoke(propName)(..args)', () => { + class CounterButton extends React.Component { + constructor(props) { + super(props); + this.state = { count: 0 }; + } + + render() { + const { count } = this.state; + return ( +
+ +
+ ); + } + } + + class ClickableLink extends React.Component { + render() { + const { onClick } = this.props; + return ( +
+ foo +
+ ); + } + } + + it('throws when pointing to a non-function prop', () => { + const wrapper = Wrap(
); + + expect(() => wrapper.invoke('data-a')).to.throw( + TypeError, + `${WrapperName}::invoke() requires the name of a prop whose value is a function`, + ); + + expect(() => wrapper.invoke('does not exist')).to.throw( + TypeError, + `${WrapperName}::invoke() requires the name of a prop whose value is a function`, + ); + }); + + it('can update the state value', () => { + const wrapper = Wrap(); + expect(wrapper.state('count')).to.equal(0); + wrapper.find('button').invoke('onClick')(); + expect(wrapper.state('count')).to.equal(1); + }); + + it('can return the handlers’ return value', () => { + const sentinel = {}; + const spy = sinon.stub().returns(sentinel); + + const wrapper = Wrap(); + + const value = wrapper.find('a').invoke('onClick')(); + expect(value).to.equal(sentinel); + expect(spy).to.have.property('callCount', 1); + }); + + it('can pass in arguments', () => { + const spy = sinon.spy(); + + const wrapper = Wrap(); + + const a = {}; + const b = {}; + wrapper.find('a').invoke('onClick')(a, b); + expect(spy).to.have.property('callCount', 1); + const [[arg1, arg2]] = spy.args; + expect(arg1).to.equal(a); + expect(arg2).to.equal(b); + }); + }); +} diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js index ce44e22f5..d9512074e 100644 --- a/packages/enzyme/src/ReactWrapper.js +++ b/packages/enzyme/src/ReactWrapper.js @@ -827,6 +827,27 @@ class ReactWrapper { return this.props()[propName]; } + /** + * Used to invoke a function prop. + * Will invoke an function prop and return its value. + * + * @param {String} propName + * @returns {Any} + */ + invoke(propName) { + return this.single('invoke', () => { + const handler = this.prop(propName); + if (typeof handler !== 'function') { + throw new TypeError('ReactWrapper::invoke() requires the name of a prop whose value is a function'); + } + return (...args) => { + const response = handler(...args); + this[ROOT].update(); + return response; + }; + }); + } + /** * Returns a wrapper of the node rendered by the provided render prop. * diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index c358fac71..99941e553 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -1295,6 +1295,27 @@ class ShallowWrapper { return this.props()[propName]; } + /** + * Used to invoke a function prop. + * Will invoke an function prop and return its value. + * + * @param {String} propName + * @returns {Any} + */ + invoke(propName) { + return this.single('invoke', () => { + const handler = this.prop(propName); + if (typeof handler !== 'function') { + throw new TypeError('ShallowWrapper::invoke() requires the name of a prop whose value is a function'); + } + return (...args) => { + const response = handler(...args); + this[ROOT].update(); + return response; + }; + }); + } + /** * Returns a wrapper of the node rendered by the provided render prop. *