Skip to content

Commit

Permalink
[New] shallow: Add invoke(eventName, ...args)
Browse files Browse the repository at this point in the history
  • Loading branch information
Miles Johnson authored and ljharb committed May 16, 2017
1 parent ab86297 commit 258823f
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 0 deletions.
1 change: 1 addition & 0 deletions SUMMARY.md
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions 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 (
<a onClick={() => this.loadData()}>
Load more
</a>
);
}
}

const wrapper = shallow(<Foo />);

wrapper.invoke('click').then(() => {
// expect()
});
```

#### Related Methods

- [`.simulate(event[, data]) => Self`](simulate.md)
3 changes: 3 additions & 0 deletions docs/api/shallow.md
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Expand Up @@ -1203,6 +1203,7 @@ describe('shallow', () => {
'hostNodes',
'html',
'instance',
'invoke',
'is',
'isEmpty',
'isEmptyRender',
Expand Down
224 changes: 224 additions & 0 deletions 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 (<a onClick={spy}>foo</a>);
}
}

const wrapper = shallow(<Foo />);
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 (
<div onClick={this.incrementCount}>
<a
className={`clicks-${count}`}
onClick={this.incrementCount}
>
foo
</a>
</div>
);
}
}

const wrapper = shallow(<Foo />);

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 (
<a onClick={spy}>foo</a>
);
}
}

const wrapper = shallow(<Foo />);
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 }) => (
<div onClick={onClick}>
<a onClick={onClick}>foo</a>
</div>
);

const wrapper = shallow(<Foo onClick={spy} />);

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 = () => (
<a onClick={spy}>foo</a>
);

const wrapper = shallow(<Foo />);
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 (<a onClick={clickSpy} onDoubleClick={spy}>foo</a>);
}
}

const wrapper = shallow(<SpiesOnClicks />);

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 (<a onMouseEnter={spy}>foo</a>);
}
}

const wrapper = shallow(<Foo />);

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 = () => (
<a onMouseEnter={spy}>foo</a>
);

const wrapper = shallow(<Foo />);

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 (
<a onClick={this.onClick}>{count}</a>
);
}
}

const wrapper = shallow(<Foo />);
wrapper.invoke('click');
expect(wrapper.text()).to.equal('1');
expect(renderCount).to.equal(2);
});
});
}
26 changes: 26 additions & 0 deletions packages/enzyme/src/ShallowWrapper.js
Expand Up @@ -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.
Expand Down

0 comments on commit 258823f

Please sign in to comment.