I have been working on a React app with a new team and the discussion came up around writing unit tests for components that trigger methods on window.scroll events.
So, let's take this component as an example.
import React, { Component } from 'react';
class MyComponent extends Component {
componentDidMount() {
window.addEventListener('scroll', this.props.myScrollMethod);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.props.myScrollMethod);
}
render() {
return (
<div>
<h1>Hello MyComponent!</h1>
</div>
)
};
};
export default MyComponent;
As you can see, I am taking a method which is passed into the component via a prop and binding it to the window event listener, where the event is scroll
. In the real world this component would call myScrollMethod
as the user is scrolling down a page (let's assume the use case here is to show a sticky navigation bar when the user has scrolled beyond a certain point on the page).
The problem is...I need to find a suitable way of testing this. My end goal is to create a spy method which is passed into the component via the myScrollMethod
prop, then trigger a scroll (or mock a scroll in the test) and finally assert whether the scroll handler method has fired or not. Here is my attempt at this:
import React from 'react';
import sinon from 'sinon';
import expect, { createSpy } from 'expect';
import { shallow } from 'enzyme';
import MyComponent from './MyComponent';
describe('The <MyComponent /> component', () => {
let onScroll;
let MyTestComponent;
beforeEach(() => {
onScroll = createSpy();
MyTestComponent = shallow(
<MyComponent
myScrollMethod={onScroll}
/>
);
});
it('Should call the onScroll method when a user scrolls', () => {
expect(onScroll).toNotHaveBeenCalled();
window.dispatchEvent(new window.UIEvent('scroll', { detail: 0 }));
expect(onScroll).toHaveBeenCalled();
});
});
The issue that I am having is that the final assertion is failing because the spy is never called. I've referred to a number of other posts on this site but so far have not found a suitable solution. Any suggestions would be greatly appreciated as it's been racking my brain for a while now!
Thanks very much!
To get scroll position with React, we can add a scroll listener to window . to create the scrollPosition state with the useState hook. Then we add the handleScroll function that takes the scroll position with the window.
forEach(element => { window. addEventListener( "scroll", () => runOnScroll(element), { passive: true } ); }); Or alternatively, bind a single scroll listener, with evt => runOnScroll(evt) as handler and then figure out what to do with everything in elements inside the runOnScroll function instead.
Unfortunately, I don't think Enzyme is going to be much help here. The library only handles events within React's synthetic event system. So a component you render with Enzyme is not going to work with event listeners added to the window. This issues thread on Enzyme's github gives more details and there are some suggested workarounds that might help you.
For example, you might want to spy on window.addEventListener
, then you can check on mount that it was called with the arguments "scroll"
and your callback.
Regarding your specific code, the scroll listener is set in componentDidMount
but your component is shallow rendered, so componentDidMount
isn't actually called (so there is no listener). Try adding this line to your beforeEach
: MyTestComponent.instance().componentDidMount()
You could make the case that events are just simple messages - since enzyme is using JSDOM under the hood, you can adequately track these messages as they are attached to nodes using plain javascript, regardless of whether the event is 'scroll', 'foo', or 'bar'.
In a test environment, we don't really care the event is called, the system just has to know how to respond it.
Heres an example of tracking non-synthetic events like scroll with enzyme:
// scollable.js
class Scrollable extends Component {
componentDidMount() {
if (this.myCustomRef) {
this.myCustomRef.addEventListener('scroll', this.handleScroll)
}
}
handleScroll = (e) => this.props.onScroll(e)
}
// scollable-test.js
import React from 'react'
import { mount } from 'enzyme'
import Scrollable from '../Scrollable'
describe('shared/Scrollable', () => {
it('triggers handler when scrolled', () => {
const onScroll = jest.fn()
const wrapper = mount(
<Scrollable onScroll={onScroll}><div /></Scrollable>
)
const customEvent = new Event('scroll')
// the first element is myCustomRef
wrapper.first().getDOMNode().dispatchEvent(customEvent)
expect(wrapper.prop('onScroll')).toHaveBeenCalled()
})
})
After we attach the events to the dom, we can trigger their handlers by using getDOMNode and dispatchEvent, which fires our prop onScroll
There are some limitations here with JSDOM, in that if you need to do things like track the size or height, or scrollTop of a node after an event has fired, you are out of luck - this is because JSDOM doesn't actually render the page for you, but instead 'emulates' the DOM for usage with libs like enzyme - It could also be argued that a test with these needs would be better suited for end to end testing, a headless browser, or entirely different tooling.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With