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

shallow wrapper .simulate() SyntheticEvent and event propagation #368

Closed
wants to merge 14 commits into from
3 changes: 2 additions & 1 deletion docs/README.md
Expand Up @@ -44,6 +44,7 @@
* [instance()](/docs/api/ShallowWrapper/instance.md)
* [is(selector)](/docs/api/ShallowWrapper/is.md)
* [isEmpty()](/docs/api/ShallowWrapper/isEmpty.md)
* [invoke(event[, ...args])](/docs/api/ShallowWrapper/invoke.md)
* [key()](/docs/api/ShallowWrapper/key.md)
* [last()](/docs/api/ShallowWrapper/last.md)
* [map(fn)](/docs/api/ShallowWrapper/map.md)
Expand All @@ -61,7 +62,7 @@
* [setProps(nextProps)](/docs/api/ShallowWrapper/setProps.md)
* [setState(nextState[, callback])](/docs/api/ShallowWrapper/setState.md)
* [shallow([options])](/docs/api/ShallowWrapper/shallow.md)
* [simulate(event[, data])](/docs/api/ShallowWrapper/simulate.md)
* [simulate(event[, mock])](/docs/api/ShallowWrapper/simulate.md)
* [slice([begin[, end]])](/docs/api/ShallowWrapper/slice.md)
* [some(selector)](/docs/api/ShallowWrapper/some.md)
* [someWhere(predicate)](/docs/api/ShallowWrapper/someWhere.md)
Expand Down
51 changes: 51 additions & 0 deletions docs/api/ShallowWrapper/invoke.md
@@ -0,0 +1,51 @@
# `.invoke(event[, ...args]) => Self`

Invoke event handlers


#### Arguments

1. `event` (`String`): The event name to be invoked
2. `...args` (`Any` [optional]): Arguments that will be passed to the event handler



#### Returns

`ShallowWrapper`: Returns itself.



#### Example

```jsx
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
const { count } = this.state;
return (
<div>
<div data-clicks={count}>
{count} clicks
</div>
<a onClick={() => this.setState({ count: count + 1 })}>
Increment
</a>
</div>
);
}
}

const wrapper = shallow(<Foo />);

expect(wrapper.find('[data-clicks=0]').length).to.equal(1);
wrapper.find('a').invoke('click');
expect(wrapper.find('[data-clicks=1]').length).to.equal(1);
```

#### Related Methods

- [`.simulate(event[, mock]) => Self`](simulate.md)
18 changes: 10 additions & 8 deletions docs/api/ShallowWrapper/simulate.md
@@ -1,13 +1,14 @@
# `.simulate(event[, ...args]) => Self`
# `.simulate(event[, mock]) => Self`

Simulate events
Simulate events. The propagation behavior of browser events is simulated — however,
this method does not emit an actual event. Event handlers are called with a SyntheticEvent object.


#### Arguments

1. `event` (`String`): The event name to be simulated
2. `...args` (`Any` [optional]): A mock event object that will get passed through to the event
handlers.
2. `mock` (`Object` [optional]): A mock event object that will be merged with the event
object passed to the handlers.



Expand Down Expand Up @@ -50,9 +51,10 @@ expect(wrapper.find('.clicks-1').length).to.equal(1);

#### Common Gotchas

- Currently, event simulation for the shallow renderer does not propagate as one would normally
expect in a real environment. As a result, one must call `.simulate()` on the actual node that has
the event handler set.
- Even though the name would imply this simulates an actual event, `.simulate()` will in fact
- Even though the name would imply this simulates an actual event, `.simulate()` will in fact
target the component's prop based on the event you give it. For example, `.simulate('click')` will
actually get the `onClick` prop and call it.

#### Related Methods

- [`.invoke(event[, ...args]) => Self`](invoke.md)
5 changes: 4 additions & 1 deletion docs/api/shallow.md
Expand Up @@ -166,9 +166,12 @@ Returns the named prop of the current node.
#### [`.key() => String`](ShallowWrapper/key.md)
Returns the key of the current node.

#### [`.simulate(event[, data]) => ShallowWrapper`](ShallowWrapper/simulate.md)
#### [`.simulate(event[, mock]) => Self`](ShallowWrapper/simulate.md)
Simulates an event on the current node.

#### [`.invoke(event[, ...args]) => Self`](ShallowWrapper/invoke.md)
Invokes an event handler on the current node.

#### [`.setState(nextState) => ShallowWrapper`](ShallowWrapper/setState.md)
Manually sets state of the root component.

Expand Down
66 changes: 56 additions & 10 deletions src/ShallowWrapper.js
@@ -1,4 +1,5 @@
import React from 'react';
import objectAssign from 'object.assign';
import flatten from 'lodash/flatten';
import unique from 'lodash/uniq';
import compact from 'lodash/compact';
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
renderToStaticMarkup,
batchedUpdates,
isDOMComponentElement,
SyntheticEvent,
} from './react-compat';

/**
Expand Down Expand Up @@ -598,26 +600,70 @@ class ShallowWrapper {
}

/**
* Used to simulate events. Pass an eventname and (optionally) event arguments. This method of
* testing events should be met with some skepticism.
* Used to invoke event handlers. Pass an eventname and (optionally) event
* arguments. This method of testing events should be met with some
* skepticism.
*
* @param {String} event
* @param {Array} args
* @returns {ShallowWrapper}
*/
simulate(event, ...args) {
const handler = this.prop(propFromEvent(event));
if (handler) {
invoke(event, ...args) {
return this.single('invoke', () => {
const handler = this.prop(propFromEvent(event));
if (handler) {
withSetStateAllowed(() => {
batchedUpdates(() => {
handler(...args);
});
this.root.update();
});
}
return this;
});
}

/**
* Used to simulate events with propagation.
* Pass an eventname and an object with properties to assign to the event object.
* This method of testing events should be met with some skepticism.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @param {String} event
* @param {Object} mock (optional)
* @returns {ShallowWrapper}
*/
simulate(event, mock) {
return this.single('simulate', () => {
const bubbleProp = propFromEvent(event);
const captureProp = `${bubbleProp}Capture`;
const e = new SyntheticEvent(undefined, undefined, { type: event, target: {} });
objectAssign(e, mock);
withSetStateAllowed(() => {
// TODO(lmr): create/use synthetic events
// TODO(lmr): emulate React's event propagation
const handlers = [
...this.parents().map(n => n.prop(captureProp)).reverse(),
this.prop(captureProp),
this.prop(bubbleProp),
...this.parents().map(n => n.prop(bubbleProp)),
];

batchedUpdates(() => {
handler(...args);
handlers.some((handler) => {
if (handler) {
handler(e);
if (e.isPropagationStopped()) {
return true;
}
}
return false;
});
});

this.root.update();
});
}
return this;
return this;
});
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/react-compat.js
Expand Up @@ -7,6 +7,7 @@
*/

import objectAssign from 'object.assign';
import SyntheticEvent from 'react/lib/SyntheticEvent';
import { REACT013 } from './version';

let TestUtils;
Expand Down Expand Up @@ -185,4 +186,5 @@ export {
renderWithOptions,
unmountComponentAtNode,
batchedUpdates,
SyntheticEvent,
};