Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fireEvent - mouseEnter/mouseLeave not working with addEventListener #577

Comments

@greypants
Copy link

greypants commented Jan 24, 2020

  • 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.

@greypants
Copy link
Author

greypants commented Jan 27, 2020

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?

@greypants
Copy link
Author

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!

@greypants
Copy link
Author

greypants commented Jan 27, 2020

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?

@kentcdodds
Copy link
Member

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?

@weyert
Copy link
Contributor

weyert commented Jan 29, 2020

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.

@jessethomson
Copy link

jessethomson commented Feb 12, 2020

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');
});

@kentcdodds
Copy link
Member

🎉 This issue has been resolved in version 9.4.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@the-teacher
Copy link

the-teacher commented Apr 14, 2020

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! ...

@MaxJaison
Copy link

@the-teacher had same issue, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment