'Testing hover state hooks

I created this useHover hook:

import { useRef, useState, useEffect } from 'react';

export const useHover = () => {
  const [value, setValue] = useState(false);

  const ref = useRef(null);

  const handleMouseOver = () => setValue(true);
  const handleMouseOut = () => setValue(false);

  useEffect(
    // eslint-disable-next-line consistent-return
    () => {
      const node = ref.current;
      if (node) {
        node.addEventListener('mouseover', handleMouseOver);
        node.addEventListener('mouseout', handleMouseOut);

        return () => {
          node.removeEventListener('mouseover', handleMouseOver);
          node.removeEventListener('mouseout', handleMouseOut);
        };
      }
    },
    []
  );

  return [ref, value];
};

I wrote this test:

import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { fireEvent, render } from '@testing-library/react';
import { Box } from 'gestalt';
import userEvent from '@testing-library/user-event';
import { useHover } from './useHover';

describe('useHover', () => {
  it('toogles', async () => {
    const { result } = renderHook(() => useHover());

    // eslint-disable-next-line no-unused-vars
    const [ref, value] = result.current;
    expect(value).toBeFalsy();

    const Div = () => <Box ref={ref}>Test</Box>;

    render(<Div />);

    await userEvent.hover(ref.current);
    expect(value).toBeTruthy();

    fireEvent.mouseOut(ref.current);
    expect(value).toBeFalsy();
  });
});

But it fails:

21 | expect(value).toBeTruthy();



Solution 1:[1]

At first glance I see some possible issues

  1. The dependency array of the useEffect is empty, this will only fire the first time the useEffect gets called in a component.
  2. The node will always be falsy the first time the useEffect mounts, thus the dom event listeners wont be added. perhaps adding [ref.current] will solve this see point 3
  3. Its generally not a good idea to use ref.current as a dependency in a dependency array.
  4. the value of "value" wont change anyway, since it was destructured into a constant value type, it will be the initial snapshot of that value type.
  5. the dom event listeners are different events to the events "fired" in the test
  6. Suggest using a useCallback hook instead and watch the callback functions in the dependency array for your custom hook
  7. Suggest Testing the hook in an actual component, since it relies on dom events linked to a component

Please see this codeSandBox

enter image description here

Solution 2:[2]

import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useHover } from '../../src/hooks/useHover';

describe('useHover', () => {
  it('toogles', () => {
    const Div = () => {
      const [hoverRef, isHovering] = useHover();
      return (
        <div>
          <div data-testid="trigger" ref={hoverRef}>
            Trigger
          </div>
          {isHovering && <div data-testid="expected">Expected</div>}
        </div>
      );
    };

    const { queryByTestId } = render(<Div />);

    expect(queryByTestId('expected')).not.toBeInTheDocument();

    userEvent.hover(queryByTestId('trigger'));

    expect(queryByTestId('expected')).toBeInTheDocument();

    userEvent.unhover(queryByTestId('trigger'));

    expect(queryByTestId('expected')).not.toBeInTheDocument();
  });
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Anthony Cifuentes