Search code examples
reactjsjestjsenzymereact-hooksuse-effect

Testing React useEffect hook while adding eventListeners


I have a functional component in my React code as below:

const Compo = ({funcA}) => {
   useEffect(() => {
      window.addEventListener('x', funcB, false);

      return () => {
       window.removeEventListener('x', funcB, false); 
      }
   });

   const funcB = () => {funcA()};      

   return (
      <button
        onClick={() => funcA()}
      />
   );
};

Compo.propTypes = {
   funcA: func.isRequired
}

export default Compo;

I need to test the above functional component to make sure the event listeners are added and removed as mentioned in the useEffect() hook.

Here is what my test file looks like -

const addEventSpy = jest.spyOn(window, 'addEventListener');
const removeEventSpy = jest.spyOn(window, 'removeEventListener');

let props = mockProps = {funcA: jest.fn()};
const wrapper = mount(<Compo {...props} />);

const callBack = wrapper.instance().funcB;     <===== ERROR ON THIS LINE
expect(addEventSpy).toHaveBeenCalledWith('x', callBack, false);

wrapper.unmount();
expect(removeEventSpy).toHaveBeenCalledWith('x', callBack, false);

However, I get the below error on the line where I declare the 'callBack' constant (highlighted above in the code) :

TypeError: Cannot read property 'funcB' of null

Effectively, it renders the component ok, but wrapper.instance() is evaluating as null, which is throwing the above error.

Would anyone please know what am I missing to fix the above error?


Solution

  • This is my unit test strategy:

    index.tsx:

    import React, { useEffect } from 'react';
    
    const Compo = ({ funcA }) => {
      useEffect(() => {
        window.addEventListener('x', funcB, false);
    
        return () => {
          window.removeEventListener('x', funcB, false);
        };
      }, []);
    
      const funcB = () => {
        funcA();
      };
    
      return <button onClick={funcB} />;
    };
    
    export default Compo;
    

    index.spec.tsx:

    import React from 'react';
    import { mount } from 'enzyme';
    import Compo from './';
    
    describe('Compo', () => {
      afterEach(() => {
        jest.restoreAllMocks();
      });
      it('should call funcA', () => {
        const events = {};
        jest.spyOn(window, 'addEventListener').mockImplementation((event, handle, options?) => {
          events[event] = handle;
        });
        jest.spyOn(window, 'removeEventListener').mockImplementation((event, handle, options?) => {
          events[event] = undefined;
        });
    
        const mProps = { funcA: jest.fn() };
        const wrapper = mount(<Compo {...mProps}></Compo>);
        expect(wrapper.find('button')).toBeDefined();
        events['x']();
        expect(window.addEventListener).toBeCalledWith('x', expect.any(Function), false);
        expect(mProps.funcA).toBeCalledTimes(1);
    
        wrapper.unmount();
        expect(window.removeEventListener).toBeCalledWith('x', expect.any(Function), false);
      });
    });
    

    Unit test result with 100% coverage:

    PASS  src/stackoverflow/57797518/index.spec.tsx (8.125s)
      Compo
        ✓ should call funcA (51ms)
    
    -----------|----------|----------|----------|----------|-------------------|
    File       |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
    -----------|----------|----------|----------|----------|-------------------|
    All files  |      100 |      100 |      100 |      100 |                   |
     index.tsx |      100 |      100 |      100 |      100 |                   |
    -----------|----------|----------|----------|----------|-------------------|
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        9.556s
    

    Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57797518