From 258823f1f55ebe66df102dded7d2601e2b22b3f4 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 16 May 2017 15:59:23 -0700 Subject: [PATCH] [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.