React-testing-library: fireEvent - mouseEnter/mouseLeave not working with addEventListener

Created on 24 Jan 2020  Â·  8Comments  Â·  Source: testing-library/react-testing-library

  • react-testing-library version: 9.4.0
  • react version: 16.12.0
  • node version: 10.16.0
  • npm (or yarn) version: 1.21.1

Relevant code or config:

import React, { useEffect, useState, useRef } from "react";
import { render, fireEvent } from "@testing-library/react";

const HoverMe = () => {
  const ref = useRef();
  const [isMouseEntered, setIsMouseEntered] = useState(false);

  useEffect(() => {
    const setYes = () => setIsMouseEntered(true);
    const button = ref.current;
    // If you change the event to "mouseover"
    // the test passes, even if you don't update the test
    button.addEventListener("mouseenter", setYes);

    return () => {
      button.removeEventListener("mouseenter", setYes);
    };
  });

  return <button ref={ref}>{isMouseEntered ? "yes" : "no"}</button>;
};

describe("mouseenter/mouseleave bug", () => {
  test("mouseenter should update text to 'yes'", () => {
    const { getByText } = render(<HoverMe />);
    fireEvent.mouseEnter(getByText("no"));
    expect(getByText("yes")).toBeTruthy();
  });
});

What you did:

I'm using a third party library that attaches mouseenter and mouseleave events to a DOM element accessed via a React ref via HTMLElement.addEventListener. Was testing this behavior.

What happened:

Works fine in the browser, but when writing my tests, I noticed that fireEvent.mouseEnter and fireEvent.mouseLeave have no effect. What's weird is if I leave the test alone, but change the event in the component to mouseover/mouseout, it works. If I attach the events the react way, via onMouseEnter and onMouseLeave, it also works.

Reproduction:

https://codesandbox.io/s/react-testing-library-demo-37yqv?fontsize=14&hidenavigation=1&theme=dark

Problem description:

fireEvent.mouseLeave and fireEvent.mouseOver do not work when the events are added via addEventListener.

Suggested solution:

It may have something to do with this: https://reactjs.org/docs/events.html#mouse-events

If this is a React limitation, or if there's a workaround, it would be great to add it to the FAQ.

needs investigation released

Most helpful comment

:tada: This issue has been resolved in version 9.4.1 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

All 8 comments

Looking under the hood at the logic here and mouseEnter definition here, I didn't see anything that specifically looked problematic. I recreated the logic as best I could in the browser, and it seemed fine:

mouseenter

~Is it a jsdom thing?~

Upon further testing, I noticed by creating the event myself and passing it to fireEvent, DOES pass ✅ :

const mouseenter = new MouseEvent("mouseenter", {
      bubbles: false,
      cancelable: false
    });

fireEvent(getByText("no"), mouseenter);

// Passes!

Ok, found the issue. These lines re-assign mouseEnter/mouseLeave to mouseOver/mouseOut:
https://github.com/testing-library/react-testing-library/blob/master/src/pure.js#L128-L132

Hmm... I have a fuzzy understanding that React is doing some non-standard thing with these events under the hood, and that this is a workaround. What are our options here?

Hi @greypants,

Thanks for the issue.

Yeah, sorry about that. I can't remember exactly the reasons for the non-standard event stuff in React, but that's what we needed to do to get things working with react event props.

I'm not sure what to do here, and I'm afraid I don't have the bandwidth right now to look into it 😬 Sorry.

Anyone else have ideas/want to dig?

Looks like an interesting problem. I can have a look at it over the weekend or next week. I won't have time for it this Thursday or Friday. If anyone else wants to pick it up feel free.

So there are the four cases we want to work with the fireEvent utility.

  1. mouseEnter/mouseLeave with native events
  2. mouseOver/mouseOut with native events
  3. mouseEnter/mouseLeave with react events
  4. mouseOver/mouseOut with react events

Currently, number 1 does not work.

We want to make a change to fix 1, without breaking the others. Here's a test people we can use to ensure we have done that. So as long as the following tests pass, I'm assuming there shouldn't be any issues getting this change in.

(Like others, I unfortunately don't have time to look into this right now, but maybe in a couple months)

import React, { useState, useEffect, useRef } from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';

const NativeEnterLeave = () => {
    const btnRef = useRef();
    const [label, setLabel] = useState('outside');
    useEffect(() => {
        const btn = btnRef.current;
        function handleMouseEnter() {
            setLabel('inside');
        }
        function handleMouseLeave() {
            setLabel('outside');
        }
        btn.addEventListener('mouseenter', handleMouseEnter);
        btn.addEventListener('mouseleave', handleMouseLeave);
        return () => {
            btn.removeEventListener('mouseenter', handleMouseEnter);
            btn.removeEventListener('mouseleave', handleMouseLeave);
        };
    });
    return <button ref={btnRef}>{label}</button>;
};

const NativeOverOut = () => {
    const btnRef = useRef();
    const [label, setLabel] = useState('outside');
    useEffect(() => {
        const btn = btnRef.current;
        function handleMouseEnter() {
            setLabel('inside');
        }
        function handleMouseLeave() {
            setLabel('outside');
        }
        btn.addEventListener('mouseover', handleMouseEnter);
        btn.addEventListener('mouseout', handleMouseLeave);
        return () => {
            btn.removeEventListener('mouseover', handleMouseEnter);
            btn.removeEventListener('mouseout', handleMouseLeave);
        };
    });
    return <button ref={btnRef}>{label}</button>;
};

const ReactEnterLeave = () => {
    const [label, setLabel] = useState('outside');
    return (
        <button
            onMouseEnter={() => setLabel('inside')}
            onMouseLeave={() => setLabel('outside')}
        >
            {label}
        </button>
    );
};

const ReactOverOut = () => {
    const [label, setLabel] = useState('outside');
    return (
        <button
            onMouseOver={() => setLabel('inside')}
            onMouseOut={() => setLabel('outside')}
        >
            {label}
        </button>
    );
};

test('fires native mouseEnter/mouseLeave events', () => {
    const { getByRole } = render(<NativeEnterLeave />);
    const btn = getByRole('button');
    expect(btn).toHaveTextContent('outside');
    fireEvent.mouseEnter(btn);
    expect(btn).toHaveTextContent('inside');
    fireEvent.mouseLeave(btn);
    expect(btn).toHaveTextContent('outside');
});

test('fires native mouseOver/mouseOut events', () => {
    const { getByRole } = render(<NativeOverOut />);
    const btn = getByRole('button');
    expect(btn).toHaveTextContent('outside');
    fireEvent.mouseOver(btn);
    expect(btn).toHaveTextContent('inside');
    fireEvent.mouseOut(btn);
    expect(btn).toHaveTextContent('outside');
});

test('fires react mouseEnter/mouseLeave events', () => {
    const { getByRole } = render(<ReactEnterLeave />);
    const btn = getByRole('button');
    expect(btn).toHaveTextContent('outside');
    fireEvent.mouseEnter(btn);
    expect(btn).toHaveTextContent('inside');
    fireEvent.mouseLeave(btn);
    expect(btn).toHaveTextContent('outside');
});

test('fires react mouseOver/mouseOut events', () => {
    const { getByRole } = render(<ReactOverOut />);
    const btn = getByRole('button');
    expect(btn).toHaveTextContent('outside');
    fireEvent.mouseOver(btn);
    expect(btn).toHaveTextContent('inside');
    fireEvent.mouseOut(btn);
    expect(btn).toHaveTextContent('outside');
});

:tada: This issue has been resolved in version 9.4.1 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

Not sure the case is relevant to the topic. But for people who look an information about fireEvent.mouseEnter by search phrase like fireEvent.mouseEnter doesn't work

In my case fireEvent.mouseEnter worked only if before I did fireEvent.mouseOver

<Cell
  onMouseEnter={() => { console.log("FIRE! ".repeat(50))}}
  data-testid='test-item'
/>
   const testItem = () => document.querySelector('[data-testid="test-item"]')

    render(<Table />)

    act(() => {
      fireEvent.mouseOver(testItem())
      fireEvent.mouseEnter(testItem())
    })

output

FIRE! FIRE! FIRE! FIRE! FIRE! FIRE! FIRE! FIRE! FIRE! FIRE! ...
Was this page helpful?
0 / 5 - 0 ratings

Related issues

jaredmeakin picture jaredmeakin  Â·  3Comments

alejandronanez picture alejandronanez  Â·  4Comments

suresh777 picture suresh777  Â·  3Comments

NiGhTTraX picture NiGhTTraX  Â·  3Comments

FlorianBurgevin picture FlorianBurgevin  Â·  3Comments