diff --git a/SUMMARY.md b/SUMMARY.md
index 3200c2826..3395ad8a6 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -48,6 +48,7 @@
* [hasClass(className)](/docs/api/ShallowWrapper/hasClass.md)
* [hostNodes()](/docs/api/ShallowWrapper/hostNodes.md)
* [html()](/docs/api/ShallowWrapper/html.md)
+ * [invoke(event[, ...args])](/docs/api/ShallowWrapper/invoke.md)
* [instance()](/docs/api/ShallowWrapper/instance.md)
* [is(selector)](/docs/api/ShallowWrapper/is.md)
* [isEmpty()](/docs/api/ShallowWrapper/isEmpty.md)
diff --git a/docs/api/ShallowWrapper/invoke.md b/docs/api/ShallowWrapper/invoke.md
new file mode 100644
index 000000000..185999971
--- /dev/null
+++ b/docs/api/ShallowWrapper/invoke.md
@@ -0,0 +1,39 @@
+# `.invoke(event[, ...args]) => Any`
+
+Invokes an event handler (a prop that matches the event name).
+
+#### Arguments
+
+1. `event` (`String`): The event name to be invoked
+2. `...args` (`Any` [optional]): Arguments that will be passed to the event handler
+
+#### Returns
+
+`Any`: Returns the value from the event handler..
+
+#### Example
+
+```jsx
+class Foo extends React.Component {
+ loadData() {
+ return fetch();
+ }
+ render() {
+ return (
+ this.loadData()}>
+ Load more
+
+ );
+ }
+}
+
+const wrapper = shallow();
+
+wrapper.invoke('click').then(() => {
+ // expect()
+});
+```
+
+#### Related Methods
+
+- [`.simulate(event[, data]) => Self`](simulate.md)
diff --git a/docs/api/shallow.md b/docs/api/shallow.md
index 77065a61a..c1e53a77b 100644
--- a/docs/api/shallow.md
+++ b/docs/api/shallow.md
@@ -180,6 +180,9 @@ Returns the key of the current node.
#### [`.simulate(event[, data]) => ShallowWrapper`](ShallowWrapper/simulate.md)
Simulates an event on the current node.
+#### [`.invoke(event[, ...args]) => Any`](ShallowWrapper/invoke.md)
+Invokes an event handler on the current node and returns the handlers value.
+
#### [`.setState(nextState) => ShallowWrapper`](ShallowWrapper/setState.md)
Manually sets state of the root component.
diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index 253b11de1..617843443 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -1203,6 +1203,7 @@ describe('shallow', () => {
'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..122f81587
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/invoke.jsx
@@ -0,0 +1,224 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { expect } from 'chai';
+import wrap from 'mocha-wrap';
+import sinon from 'sinon-sandbox';
+import { Portal } from 'react-is';
+
+import { render } from 'enzyme';
+import getAdapter from 'enzyme/build/getAdapter';
+import {
+ ITERATOR_SYMBOL,
+ sym,
+} from 'enzyme/build/Utils';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import realArrowFunction from '../../_helpers/realArrowFunction';
+import { getElementPropSelector, getWrapperPropSelector } from '../../_helpers/selectors';
+import {
+ is,
+ REACT16,
+} from '../../_helpers/version';
+
+import {
+ createClass,
+ createPortal,
+ createRef,
+ Fragment,
+} from '../../_helpers/react-compat';
+
+export default function describeInvoke({
+ Wrap,
+ WrapRendered,
+ Wrapper,
+ WrapperName,
+ isShallow,
+ isMount,
+ makeDOMElement,
+}) {
+ describe('.invoke(eventName, ..args)', () => {
+ it('should return the handlers return value', () => {
+ const spy = sinon.stub().returns(123);
+ class Foo extends React.Component {
+ render() {
+ return (foo);
+ }
+ }
+
+ const wrapper = shallow();
+ const value = wrapper.invoke('click');
+
+ expect(value).to.equal(123);
+ expect(spy).to.have.property('callCount', 1);
+ });
+
+ it('should invoke event handlers without propagation', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { count: 0 };
+ this.incrementCount = this.incrementCount.bind(this);
+ }
+
+ incrementCount() {
+ this.setState({ count: this.state.count + 1 });
+ }
+
+ render() {
+ const { count } = this.state;
+ return (
+
+ );
+ }
+ }
+
+ const wrapper = shallow();
+
+ expect(wrapper.find('.clicks-0').length).to.equal(1);
+ wrapper.find('a').invoke('click');
+ expect(wrapper.find('.clicks-1').length).to.equal(1);
+ });
+
+ it('should pass in arguments', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ render() {
+ return (
+ foo
+ );
+ }
+ }
+
+ const wrapper = shallow();
+ const a = {};
+ const b = {};
+
+ wrapper.invoke('click', a, b);
+ expect(spy.args[0][0]).to.equal(a);
+ expect(spy.args[0][1]).to.equal(b);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('should invoke event handlers', () => {
+ const spy = sinon.spy();
+ const Foo = ({ onClick }) => (
+
+ );
+
+ const wrapper = shallow();
+
+ expect(spy).to.have.property('callCount', 0);
+ wrapper.find('a').invoke('click');
+ expect(spy).to.have.property('callCount', 1);
+ });
+
+
+ it('should pass in arguments', () => {
+ const spy = sinon.spy();
+ const Foo = () => (
+ foo
+ );
+
+ const wrapper = shallow();
+ const a = {};
+ const b = {};
+
+ wrapper.invoke('click', a, b);
+ const [[arg1, arg2]] = spy.args;
+ expect(arg1).to.equal(a);
+ expect(arg2).to.equal(b);
+ });
+ });
+
+ describe('Normalizing JS event names', () => {
+ it('should convert lowercase events to React camelcase', () => {
+ const spy = sinon.spy();
+ const clickSpy = sinon.spy();
+ class SpiesOnClicks extends React.Component {
+ render() {
+ return (foo);
+ }
+ }
+
+ const wrapper = shallow();
+
+ wrapper.invoke('dblclick');
+ expect(spy).to.have.property('callCount', 1);
+
+ wrapper.invoke('click');
+ expect(clickSpy).to.have.property('callCount', 1);
+ });
+
+ describeIf(is('> 0.13'), 'normalizing mouseenter', () => {
+ it('should convert lowercase events to React camelcase', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ render() {
+ return (foo);
+ }
+ }
+
+ const wrapper = shallow();
+
+ wrapper.invoke('mouseenter');
+ expect(spy).to.have.property('callCount', 1);
+ });
+
+ it('should convert lowercase events to React camelcase in stateless components', () => {
+ const spy = sinon.spy();
+ const Foo = () => (
+ foo
+ );
+
+ const wrapper = shallow();
+
+ wrapper.invoke('mouseenter');
+ expect(spy).to.have.property('callCount', 1);
+ });
+ });
+ });
+
+ it('should batch updates', () => {
+ let renderCount = 0;
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ count: 0,
+ };
+ this.onClick = this.onClick.bind(this);
+ }
+
+ onClick() {
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ }
+
+ render() {
+ renderCount += 1;
+ const { count } = this.state;
+ return (
+ {count}
+ );
+ }
+ }
+
+ const wrapper = shallow();
+ wrapper.invoke('click');
+ expect(wrapper.text()).to.equal('1');
+ expect(renderCount).to.equal(2);
+ });
+ });
+}
diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js
index c358fac71..60dbdc194 100644
--- a/packages/enzyme/src/ShallowWrapper.js
+++ b/packages/enzyme/src/ShallowWrapper.js
@@ -1106,6 +1106,32 @@ class ShallowWrapper {
return this.type() === null ? cheerio() : cheerio.load('')(this.html());
}
+ /*
+ * Used to simulate events. Pass an eventname and (optionally) event arguments.
+ * Will invoke an event handler prop of the same name and return its value.
+ *
+ * @param {String} event
+ * @param {Array} args
+ * @returns {Any}
+ */
+ invoke(event, ...args) {
+ return this.single('invoke', () => {
+ const handler = this.prop(propFromEvent(event));
+ let response = null;
+
+ if (handler) {
+ withSetStateAllowed(() => {
+ performBatchedUpdates(this, () => {
+ response = handler(...args);
+ });
+ this.root.update();
+ });
+ }
+
+ return response;
+ });
+ }
+
/**
* Used to simulate events. Pass an eventname and (optionally) event arguments. This method of
* testing events should be met with some skepticism.