From 258823f1f55ebe66df102dded7d2601e2b22b3f4 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 16 May 2017 15:59:23 -0700 Subject: [PATCH 1/3] [New] `shallow`: Add `invoke(eventName, ...args)` --- SUMMARY.md | 1 + docs/api/ShallowWrapper/invoke.md | 39 +++ docs/api/shallow.md | 3 + .../test/ShallowWrapper-spec.jsx | 1 + .../test/shared/methods/invoke.jsx | 224 ++++++++++++++++++ packages/enzyme/src/ShallowWrapper.js | 26 ++ 6 files changed, 294 insertions(+) create mode 100644 docs/api/ShallowWrapper/invoke.md create mode 100644 packages/enzyme-test-suite/test/shared/methods/invoke.jsx 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 ( +
+ + foo + +
+ ); + } + } + + 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 }) => ( +
+ foo +
+ ); + + 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. From 60ea65245d288d4eb5fcb6fec286e4f0e826c07f Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sat, 6 Apr 2019 15:17:29 -0700 Subject: [PATCH 2/3] Revert "[New] `shallow`: Add `invoke(eventName, ...args)`" This reverts commit eb14926cbc5de867745ca7c892bda533ddff043b. --- SUMMARY.md | 1 - docs/api/ShallowWrapper/invoke.md | 39 --- docs/api/shallow.md | 3 - .../test/ShallowWrapper-spec.jsx | 1 - .../test/shared/methods/invoke.jsx | 224 ------------------ packages/enzyme/src/ShallowWrapper.js | 26 -- 6 files changed, 294 deletions(-) delete mode 100644 docs/api/ShallowWrapper/invoke.md delete mode 100644 packages/enzyme-test-suite/test/shared/methods/invoke.jsx diff --git a/SUMMARY.md b/SUMMARY.md index 3395ad8a6..3200c2826 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -48,7 +48,6 @@ * [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 deleted file mode 100644 index 185999971..000000000 --- a/docs/api/ShallowWrapper/invoke.md +++ /dev/null @@ -1,39 +0,0 @@ -# `.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 c1e53a77b..77065a61a 100644 --- a/docs/api/shallow.md +++ b/docs/api/shallow.md @@ -180,9 +180,6 @@ 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 617843443..253b11de1 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -1203,7 +1203,6 @@ 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 deleted file mode 100644 index 122f81587..000000000 --- a/packages/enzyme-test-suite/test/shared/methods/invoke.jsx +++ /dev/null @@ -1,224 +0,0 @@ -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 }) => ( -
- foo -
- ); - - 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 60dbdc194..c358fac71 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -1106,32 +1106,6 @@ 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. From d3d2259304aa52a204fd778b7a3ff416a9da44dc Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Sat, 9 Mar 2019 23:30:30 +0900 Subject: [PATCH 3/3] [New] `shallow`/`mount`: add `invoke(propName)(...args)` --- SUMMARY.md | 2 + docs/api/ReactWrapper/invoke.md | 41 +++++++++ docs/api/ShallowWrapper/invoke.md | 40 +++++++++ docs/api/mount.md | 3 + docs/api/shallow.md | 3 + .../test/ReactWrapper-spec.jsx | 1 + .../test/shared/methods/invoke.jsx | 88 +++++++++++++++++++ packages/enzyme/src/ReactWrapper.js | 21 +++++ packages/enzyme/src/ShallowWrapper.js | 21 +++++ 9 files changed, 220 insertions(+) create mode 100644 docs/api/ReactWrapper/invoke.md create mode 100644 docs/api/ShallowWrapper/invoke.md create mode 100644 packages/enzyme-test-suite/test/shared/methods/invoke.jsx 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. *