Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to test Window Scroll Event Handlers with Enzyme?

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!

like image 564
Jamie Bradley Avatar asked Jul 28 '17 15:07

Jamie Bradley


People also ask

How does scroll position react detect?

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.

How do I use scroll event listener?

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.


2 Answers

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()

like image 71
boylingpoynt Avatar answered Oct 17 '22 07:10

boylingpoynt


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.

like image 32
lfender6445 Avatar answered Oct 17 '22 07:10

lfender6445