Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ShallowWrapper#invoke() to invoke event handlers and return the handlers value #945

Merged
merged 1 commit into from Apr 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i kind of wonder if maybe this should just be a prop name rather than an event name, ie, onClick, not click? Then it can be used to invoke any function-valued prop.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think I agree

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for onClick rather than click.


#### 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..
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two dots at the end


#### 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“and returns the handler’s return 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