The first component I tried to test using this was a mixin for detecting clicks outside a component. In order to do this one needs to listen with window.addEventListener('click'). This handler doesn't appear to be triggered when using enzyme simulated clicks.
If handling this is out of scope for enzyme, do you have a recommendation on the best way to get this under test?
Ahh, so mount doesn't actually attach the rendered fragment to the document, so even if simulated events did bubble up through the concrete DOM, they wouldn't reach window anyway.
I found a workaround:
document.bodysimulant to fire "real" DOM events.e.g.
it('only triggers clickOutside handler when clicking outside component', t => {
const onClickOutside = sinon.spy()
mount(<Page onClickOutside={onClickOutside} />, { attachTo: document.body })
simulant.fire(document.body.querySelector('aside'), 'click')
t.equal(onClickOutside.callCount, 1, 'should fire when clicking menu sibling')
document.body.innerHTML = ''
t.end()
})
However, I've just noticed that in this particular example enzyme isn't actually doing anything haha.
We can just ReactDOM.render into our JSDOM document.body directly:
it('only triggers clickOutside handler when clicking outside component', t => {
const onClickOutside = sinon.spy()
ReactDOM.render(<Page onClickOutside={onClickOutside} />, document.body)
simulant.fire(document.body.querySelector('aside'), 'click')
t.equal(onClickOutside.callCount, 1, 'should fire when clicking menu sibling')
document.body.innerHTML = ''
t.end()
})
Curious now as to why not use just always use this method? why enzyme?
Can a core contributor respond to this?
I'm having the same issue now where instead of attaching a event on a direct element, I'm attaching it on the document so that I can easily detect a click outside of the said element. But I'm having hard time testing this behavior with enzyme.
@Aweary @lelandrichardson
@tleunen I doubt this is a use case we'd support, enzyme is meant to test React components and attaching an event listener to the document with addEventListener means the event is not being handled by React's synthetic event system. Our simulate method for mount is a thin wrapper around ReactTestUtils.Simulate, which only deals with React's synthetic event system.
I can't speak to your specific use case, but I would advise that this is generally an anti-pattern in React and should be handled within React's event system when possible (such as passing down an onClick prop from a stateful parent and calling it in the leaf component's onClick handler). You can try workaround like @timoxley but your mileage may vary.
So you would attach an onClick on the main root component, and then passing it to all components? Maybe using the context then?
That may be a good use case for context if you need to monitor click events within arbitrarily nested components.
@Aweary , would you mind giving a bit more information about how to tackle those window/body event listeners using context? I've been searching endlessly as to how's the best way of doing it and I don't have an answer yet.
I'm having exactly the same problem as @timoxley , but with a KeyPress event... It's proving to be extremely frustrating to test.
Thank you
@rpmonteiro if you were using context you wouldn't be using the native event system, you would have on onKeyPress method on your top-level component that you make available via context to child components. I'm not sure if this is the best idea, and I haven't tried it. With that disclaimer, it might look something like:
class App extends React.Component {
getChildContext() {
onKeyPress: this.onKeyPress
}
onKeyPress(event) {
// handle event here
}
}
Then in some child component
class SomeChildComponentOfApp extends React.Component {
static contextTypes = {
// the onKeyPress function is now available via `this.context.onKeyPress`
onKeyPress: React.PropTypes.func
}
}
If you absolutely must use the native event system, you might look into another library for mocking addEventListener and the related functions.
Generally any test framework(jest, mocha, etc) can solve your problem natively. Your goal here is effectively to make sure the event is bound, and that when its fired something happens in your component. So you'll have to do some setup prior to rendering, but it is definitely possible to test this code without using context.
To be clear, @aweary is spot on in saying this is not enzyme supported.
For example in jest this is a sort of code you could use.
const map = {};
Window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
map[event] = cb;
});
// render component
map.event(...args);
// assert changes
@blainekasten you saved my day :)
const map = {};
window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
map[event] = cb;
});
const component = mount(<SomeComponent />);
map.mousemove({ pageX: 100, pageY: 100});
This worked for me, the state of the component is successfully being updated.
just as a small update and FYI, for document this is working for me, and on a newer version of jest:
const map = {};
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
Component:
componentDidMount() {
ReactDOM.findDOMNode(this.datePicker.refs.input).addEventListener("change", (event) => {
const value = event.target.value;
this.handleChange(Moment(value).toISOString(), value);
});
}
Test:
it('change empty value date picker', () => {
const app = ReactTestUtils.renderIntoDocument(<Datepicker />);
const datePicker = ReactDOM.findDOMNode(app.datePicker.refs.input);
const value = "";
const event = new Event("change");
datePicker.value = value;
datePicker.dispatchEvent(event);
expect(app.state.formattedValue).toEqual(value);
});
I'm sorry I am a newbie to react and jest. I didn't understand what is happening here
const map = {};
window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
map[event] = cb; // what is cb here?
});
const component = mount(<SomeComponent />);
map.mousemove({ pageX: 100, pageY: 100}); // what is map here?
How did it effect component behaviour?
@prasadmsvs please file a new issue rather than commenting on a new one; but for this kind of question, the gitter channel linked in the readme is preferred.
Inspired by @kellyrmilligan's solution here's full implementation I use to detect ESC keydown (also useful for any other event type):
it('calls the dismiss callback on ESC key', () => {
const KEYBOARD_ESCAPE_CODE = 27;
const mockDismissCallback = jest.fn();
// Declaring keydown prop so the linter doesn't compain
const eventMap = {
keydown: null,
};
document.addEventListener = jest.fn((event, cb) => {
eventMap[event] = cb;
});
// MyModalComponent internally uses
// document.addEventListener('keydown', this.onModalDialogKeyDown, false);
// which then via onModalDialogKeyDown binding does some stuff and then calls onDismiss which
// is really mockDismissCallback
const modal = shallow(
<MyModalComponent isOpen={true} onDismiss={mockDismissCallback}>
Test
</MyModalComponent>
);
eventMap.keydown({ keyCode: KEYBOARD_ESCAPE_CODE });
expect(mockDismissCallback.mock.calls.length).toEqual(1);
});
Testing a React app I had to simulate a string of text input, arrived at this solution using sinon:
const sandbox = sinon.createSandbox();
let handlers = [];
const fakeAddListener = (type, handler) => handlers.push(handler);
const dispatchKeypress = (e) => handlers.forEach(h => {h(e)});
// My handlers only examine the `which` property, but construct event with the properties you need
const createKeypressEvent = (c) => ({ which: c.charCodeAt(0) });
const simulateKeyStrokes = (textString) => [...textString].forEach(c => dispatchKeypressEvent(createKeypressEvent(c)));
beforeEach(() => {
handlers = [];
sandbox.stub(document, 'addEventListener').callsFake(fakeAddListener);
});
afterEach(() => {
sandbox.restore();
});
it('simulates a bunch of key strokes', () => {
// instantiate react component test object here that attaches listeners
// setup any stubs/spies
simulateKeyStrokes('abcdefghijklmnopqrstuvwxyz');
// handle assertions/expects
});
This also works without having to mock window.addEventListener, due to all the reasons mentioned above. Test framework: jest + enzyme.
test('should close when user clicks outside it', () => {
const outerNode = document.createElement('div');
document.body.appendChild(outerNode);
const onClose = jest.fn();
const wrapper = mount(<Toast onClose={ onClose } />, { attachTo: outerNode });
const toast = wrapper.find(`[data-test-id="toast"]`); // getDOMNode() could also work
toast.instance().dispatchEvent(new Event('click'));
expect(onClose).not.toHaveBeenCalled();
outerNode.dispatchEvent(new Event('click'));
expect(onClose).toHaveBeenCalled();
});
A little further explanation: wrapper.find().instance() returns a DOM element (whereas wrapper.instance() would just return the Toast class)--this gives us access to EventTarget.dispatchEvent(), which you can use to dispatch non-synthetic events, rather than mocking out window.addEventListener.
And by adding another div to the document.body, then attaching the mounted wrapper, you ensure the real element event will bubble up to the actual window. (Assumption is that you have a click listener on the window.) Note that I also tried attaching directly to the document.body, as in the first comment, but React then throws "Rendering components directly into document.body is discouraged."
@Faline10, that's awesome! As a note to others, I had to do .dispatchEvent(new Event('click', { bubbles: true})) to make it bubble to the window
@Faline10, that's awesome! As a note to others, I had to do
.dispatchEvent(new Event('click', { bubbles: true}))to make it bubble to the window
ev = new Event('click');
I am getting Event is undefined.
@Faline10 Trying to implement your method. But sporadically running into this error when the test is run.
TypeError: p.instance(...).dispatchEvent is not a function
And here is the test.
it('clicking outside of cell deselects cell', () => {
const callBack = jest.fn();
const thisWrapper = mount(
<ObjectItemEditableSelectCell
{...mockConnectProps}
handleClick={callBack}
/>,
);
const p = thisWrapper.find(Layout);
console.log('p.debug()', p.debug());
p.instance().dispatchEvent(new Event('click'), { bubbles: true } );
thisWrapper.update();
expect(thisWrapper.state('selectedCell')).toBe(true);
});
Would be grateful to anyone who can help.
@feargalObo In this case, p.instance() won't be an HTML element, it will be a Layout component instance. You may want to locate a specific DOM element first.
@feargalObo let's move these comments to a new issue.
Most helpful comment
Generally any test framework(jest, mocha, etc) can solve your problem natively. Your goal here is effectively to make sure the event is bound, and that when its fired something happens in your component. So you'll have to do some setup prior to rendering, but it is definitely possible to test this code without using context.
To be clear, @aweary is spot on in saying this is not enzyme supported.
For example in jest this is a sort of code you could use.