16

I hope someone can point me in the right direction to test useRef in the component below.

I have a component structured something like below. I am trying to test the functionality within the otherFunction() but I'm not sure how to mock the current property that comes off the component ref. Has anyone done something like this before?

const Component = (props) => {
    const thisComponent = useRef(null);
    const otherFunction = ({ current, previousSibling  }) => {
        if (previousSibling) return previousSibling.focus();
        if (!previousSibling && current) return current.focus();
    } 
    const handleFocus = () => {
        const {current} = thisComponent;
        otherFunction(current);
    }
     return (
        <div ref={thisComponent} onFocus={handleFocus}>Stuff In here</div>
    );
};
Lin Du
  • 88,126
  • 95
  • 281
  • 483
Jordan Melendez
  • 171
  • 1
  • 1
  • 4
  • there is definitely no way to inject/modify function's closures(and `thisComponent` is available through closures in `handleFocus`). if `.focus()` is called properly to you(I mean there is no error) you may try to put mock on `HTMLElement.prototype.focus` – skyboyer Jun 26 '19 at 09:28
  • This makes sense. Thanks for the input! – Jordan Melendez Jul 02 '19 at 17:48

1 Answers1

11

Here is my test strategy for your case. I use jest.spyOn method to spy on React.useRef hook. It will let us mock the different return value of ref object for SFC.

index.tsx:

import React, { RefObject } from 'react';
import { useRef } from 'react';

export const Component = props => {
  const thisComponent: RefObject<HTMLDivElement> = useRef(null);
  const otherFunction = ({ current, previousSibling }) => {
    if (previousSibling) return previousSibling.focus();
    if (!previousSibling && current) return current.focus();
  };
  const handleFocus = () => {
    const { current } = thisComponent;
    const previousSibling = current ? current.previousSibling : null;
    otherFunction({ current, previousSibling });
  };
  return (
    <div ref={thisComponent} onFocus={handleFocus}>
      Stuff In here
    </div>
  );
};

index.spec.tsx:

import React from 'react';
import { Component } from './';
import { shallow } from 'enzyme';

describe('Component', () => {
  const focus = jest.fn();
  beforeEach(() => {
    jest.restoreAllMocks();
    jest.resetAllMocks();
  });
  test('should render correctly', () => {
    const wrapper = shallow(<Component></Component>);
    const div = wrapper.find('div');
    expect(div.text()).toBe('Stuff In here');
  });
  test('should handle click event correctly when previousSibling does not exist', () => {
    const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: { focus } });
    const wrapper = shallow(<Component></Component>);
    wrapper.find('div').simulate('focus');
    expect(useRefSpy).toBeCalledTimes(1);
    expect(focus).toBeCalledTimes(1);
  });

  test('should render and handle click event correctly when previousSibling exists', () => {
    const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: { previousSibling: { focus } } });
    const wrapper = shallow(<Component></Component>);
    wrapper.find('div').simulate('focus');
    expect(useRefSpy).toBeCalledTimes(1);
    expect(focus).toBeCalledTimes(1);
  });

  test('should render and handle click event correctly when current does not exist', () => {
    const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: null });
    const wrapper = shallow(<Component></Component>);
    wrapper.find('div').simulate('focus');
    expect(useRefSpy).toBeCalledTimes(1);
    expect(focus).not.toBeCalled();
  });
});

Unit test result with 100% coverage:

 PASS  src/stackoverflow/56739670/index.spec.tsx (6.528s)
  Component
    ✓ should render correctly (10ms)
    ✓ should handle click event correctly when previousSibling does not exist (3ms)
    ✓ should render and handle click event correctly when previousSibling exists (1ms)
    ✓ should render and handle click event correctly when current does not exist (2ms)

-----------|----------|----------|----------|----------|-------------------|
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:       4 passed, 4 total
Snapshots:   0 total
Time:        7.689s

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

Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • 5
    NOTE: this only works when using `const ref = React.useRef(..)` as opposed to `const ref = useRef(..)` as of jest 24.9. – ragurney Mar 31 '20 at 17:53
  • 1
    It's not working for me. It is still calling the action function. Same happens for useContext. Can you share your package.json? – Shivang Gupta May 11 '20 at 08:56
  • @ShivangGupta I had given the source code, you can check it. If you have a new question, please create a new question and provide a minimal, complete, reproducible code. – Lin Du May 11 '20 at 09:01
  • @ShivangGupta check this one: https://stackoverflow.com/a/61789113/6463558 – Lin Du May 14 '20 at 04:03
  • @slideshowp2 i have a similiar problem , i am trying to test an onClick on the div with a hidden file input having the 'ref' `<>
    >` . i am getting " cannot read property of null " on simulating click. How to solve it ?
    – roma Mar 14 '22 at 15:35
  • @roma Please ask a new question. – Lin Du Mar 15 '22 at 01:56
  • In reference to a comment above, about the `spyOn` not working if you are using `const ref = useRef(..)`. If you import React into your test file as `import * as React from "react"` the `spyOn` will work. – tallpaul Nov 30 '22 at 09:05