Skip to content

Not firing onChange event #359

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

Closed
fgarcia opened this issue Apr 20, 2019 · 25 comments
Closed

Not firing onChange event #359

fgarcia opened this issue Apr 20, 2019 · 25 comments

Comments

@fgarcia
Copy link

fgarcia commented Apr 20, 2019

  • react-testing-library version: 6.1.2
  • react version: 16.8.6
  • node version: 10.13.0
  • npm (or yarn) version: 6.9

Relevant code or config:

  import Input from '@material-ui/core/Input'

  it.only('x - demo test', async () => {
    let onChange = () => console.log('did change!!!')
    const View = () => {
      return (
        <div>
          <Input
            id="login"
            name="login"
            onChange={onChange}
            inputProps={{ 'data-testid': 'foo-inside' }}
            data-testid="foo-outside"
          />
        </div>
      )
    }

    let sut = render(<View />)

    let input1 = sut.container.querySelector('input[name="login"]')
    fireEvent.change(input1, { target: { value: 'panda' } })

    let input2 = sut.getByTestId('foo-inside')
    fireEvent.change(input2, { target: { value: 'panda' } })
  })

What you did:

I've seen other issues with problems where Material UI, they seem to have solved the problem but I cannot get this to work.

I tried two ways to change the right DOM element. If I search wit wrong test-id (to get the layout) I get this:

<body>
  <div>
    <div>
      <div
        class="MuiInputBase-root-104 MuiInput-root-91 MuiInput-underline-95"
        data-testid="foo-outside"
      >
        <input
          class="MuiInputBase-input-114 MuiInput-input-99"
          data-testid="foo-inside"
          id="login"
          name="login"
          type="text"
          value=""
        />
      </div>
    </div>
  </div>
</body>

Reproduction:

git clone https://github.com/fgarcia/react-test-issue

npm start

Problem description:

I have a controlled input element and I cannot capture the onChange event.

I would expect the let onChange function of the example to be called

@vripoche
Copy link

Hello,

I confirm that onChange() is triggered in CodeSandbox but never with JSDOM (v14.1.0 and 15.0.0).
onClick() works well with JSDOM.

I've tried with:

input.value = "new"
fireEvent.change(input)
// or
fireEvent.change(input, { target: { value: 'new' } })
// or with userEvent
userEvent.type(input, 'new')

Neither works, with controlled or uncontrolled inputs.

// FIRED is not displayed
<input name="title" type="text" id="title" onChange={() => console.log('FIRED')} />

// value is equal to "new", not to "forced"
<input name="title" type="text" id="title" onChange={event=> {
  event.preventDefault()
  setTitle('forced')
}} value={title} />

@vripoche
Copy link

Workaround

with Simulate.change(input) from ReactTestUtils, the onChange() is triggered in JSDOM.

@kentcdodds
Copy link
Member

I'm not sure what the problem is exactly, but when I try it with jest it works properly: https://github.com/kentcdodds/react-test-issue-with-jest

So my guess is you have JSDOM misconfigured. The good news is there's hope and you just need to figure out how to configure JSDOM correctly. Good luck!

@vripoche
Copy link

Hi @kentcdodds,

Really thank you for your example I was able to compare configurations.
I use official jsdom package and react-scripts use jest-environment-jsdom-fourteen, a fork, so if I change it in my project it also works.
The official JSDOM version is still compliant to node 6 and Jest not, I do not know exactly why it is related to change triggering but it is :-)

Explanation why Jest team prefers use their JSDOM fork without node 6 compatibility:
testing-library/dom-testing-library#115 (comment)

@fgarcia
Copy link
Author

fgarcia commented Apr 26, 2019

My problem is yet to be solved. Unfortunately I never I had to move a project into jest and create-react-app. Sorry for the silly question but how did you actually run the test? My first thoughts were npm test and jest file but it did not work.

I will come back later when I manage the time to deal with jest. I really think this project has a better approach than enzyme. I tried to follow the links but I started to get some abstraction layers on how to configure JSDOM. It just will need more time to reverse engineer

Thanks for trying the material-ui code since it is not related to this project. I hope for future versions to become less Jest dependent. I would even like to promote my template for issues since it is the bare minimum. It does not require a testing library (run the file directly) and exposes clearly the critical dependency (JSDOM) which tends to have a different configuration among projects.

well, JSDOM has been a source of pain and magic hacks since I deal with it, but it is the necessary evil

@kentcdodds
Copy link
Member

the critical dependency (JSDOM) which tends to have a different configuration among projects.

I'd suggest that 90% of people using react-testing-library are using it with jest and they're using jest's default JSDOM functionality, so we're not going to change the template. I suggest you use Jest with it's default configuration for jsdom and you'll be fine.

Thanks though!

@TidyIQ
Copy link

TidyIQ commented May 6, 2019

I'm also having the same issue.

This doesn't work. It returns a different color to what is returned when it's used in production:

  describe("Valid input", () => {
    it("sets border color to theme success color", () => {
      const { getByLabelText, container } = render(
        <OutlinedInput {...mockProps} />,
        {}
      );
      const input = getByLabelText(mockProps.label);
      fireEvent.change(input, { target: { value: "Valid input value" } });
      const fieldset = container.querySelector("fieldset");
      const inputStyles = window.getComputedStyle(fieldset);
      expect(inputStyles.getPropertyValue("border-color")).toBe(
        theme.custom.palette.success
      );
    });
  });

This works fine. It returns the same value that's returned when used in production:

  describe("Focused", () => {
    it("sets border color to theme primary color", () => {
      const { getByLabelText, container } = render(
        <OutlinedInput {...mockProps} />,
        {}
      );
      const input = getByLabelText(mockProps.label);
      fireEvent.focus(input);
      const fieldset = container.querySelector("fieldset");
      const inputStyles = window.getComputedStyle(fieldset);
      expect(inputStyles.getPropertyValue("border-color")).toBe(
        theme.palette.primary.main
      );
    });
  });

@kentcdodds
Copy link
Member

Maybe @oliviertassinari can help with this

@oliviertassinari
Copy link

oliviertassinari commented May 6, 2019

@TidyIQ You can find a working Input change test in https://codesandbox.io/s/nn3o075794.

Let's see for the OutlinedInput. Could you provide a failing test case?

@TidyIQ
Copy link

TidyIQ commented May 7, 2019

@oliviertassinari Sure.

Here's the onChange function for the OutlinedInput component. It changes the field's value state to the user's input. It works as expected in production and I can see that the "value" attribute for <input> changes on user input when inspected in Chrome.

  const handleChange: OutlinedInputProps["onChange"] = event => {
    dispatch(
      updateString(`${fieldStateString}.${id}.value`, event.target.value)
    );
  };

This is the entire <OutlinedInput> component. I'll post a simplified version below that shows the relevant parts as there's a lot going on here. Again, this works as expected in production.

      <OutlinedInput
        id={id}
        value={value}
        classes={{
          notchedOutline: classes.notchedOutline,
          root: !isEmpty ? classes.notchedValid : undefined,
          focused: classes.focused
        }}
        labelWidth={labelWidth}
        notched={!isEmpty}
        type={thisType}
        inputProps={{
          className: classes.inputProps
        }}
        startAdornment={
          startAdornmentIcon ? (
            <IconStartAdorn
              icon={startAdornmentIcon}
              props={{
                classes:
                  isInvalid && isEmpty
                    ? { root: classes.errorColor }
                    : !isEmpty
                    ? { root: classes.validColor }
                    : undefined,
                ...startAdornIconProps
              }}
            />
          ) : (
            undefined
          )
        }
        endAdornment={
          type === "password" && showPasswordStateString !== undefined ? (
            <PasswordIconButton
              stateString={showPasswordStateString}
              iconProps={passwordIconProps}
            />
          ) : (
            undefined
          )
        }
        onChange={handleChange}
        data-testid="outlinedInputInput"
        {...inputProps}
      />

Simplified:

// Relevant CSS:
    notchedOutline: {},
    notchedValid: {
      "& $notchedOutline": {
        borderColor: theme.custom.palette.success,
      },
      "&:hover $notchedOutline": {
        borderColor: theme.custom.palette.success
      },
      "&$focused $notchedOutline": {
        borderColor: theme.custom.palette.success,
      }
    },
    focused: {}

// const value = thisFieldState.value
// const isEmpty = value.length === 0

      <OutlinedInput
        value={value} // This works as expected
        labelWidth={100}
        onChange={handleChange} // This works as expected.
        classes={{
          notchedOutline: classes.notchedOutline,
          root: !isEmpty ? classes.notchedValid : undefined, // This changes the <fieldset> border color to theme.custom.palette.success if the input field is not empty. It works as expected.
          focused: classes.focused
        }}
      />

And here's the test that's failing:

    it("sets border color to theme success color", async (): Promise<void> => {
      // Render component
      const { getByLabelText, container } = render(
        <OutlinedInput {...mockProps} />,
        {}
      );

      // Get <OutlinedInput> component
      const input: HTMLElement = getByLabelText(mockProps.label);

      // Get <fieldset> component
      // <fieldset> is the component that displays the <OutlinedInput> border-color
      const fieldset: HTMLElement = container.querySelector("fieldset");

      // Add a valid input to <OutlinedInput>
      fireEvent.change(input, {
        target: { value: "Valid input value" }
      });

      // <input value="..."> should change after fireEvent.change() is called above
      const fieldSetBorderColor: string = await waitForDomChange({ input })
        .then(
          (): string =>
            window.getComputedStyle(fieldset).getPropertyValue("border-color")
        )
        .catch((error: Error): string => error.message);

      // Test for successful change
      expect(fieldSetBorderColor).toBe(theme.custom.palette.success);
    });

Here's the failed message:

    expect(received).toBe(expected) // Object.is equality

    Expected: "#2e7d32"
    Received: "Timed out in waitForDomChange."

And if I remove the waitForDomChange and just get the fieldSetBorderColor directly, the failed message is:

    expect(received).toBe(expected) // Object.is equality

    Expected: "#2e7d32"
    Received: "threedface"

And if I run fireEvent.focus(input) instead, it works as expected and returns the correct border-color value when the input is focused (theme.palette.primary.main).

    expect(received).toBe(expected) // Object.is equality

    Expected: "#2e7d32"
    Received: "#1976d2"

@mpopv
Copy link

mpopv commented Jun 21, 2019

@fgarcia @TidyIQ I ran into this same issue testing a MaterialUI Select.

MaterialUI attaches the event handler to a div that wraps the input, not the input itself, so you can't update the component by triggering the input's onChange.

You could replicate MaterialUI's own test -- click the element, and then click an option.

Or you could add native={true} if you're willing to use a native browser <select>.

@oliviertassinari
Copy link

oliviertassinari commented Jun 21, 2019

@mpopv The input is <input type="hidden">, it's only here for interop with an old school form POST. ⚠ It shouldn't be used by the tests.

Use the native version of the Select component (uses the <select> element) if you want to trigger a change event.

@jamesBennett
Copy link

jamesBennett commented Nov 11, 2019

Why is this closed? Many seem to be still seeing this issue.
I'm using the latest jsdom, jest, react-testing-library
I'm seeing the same thing as other people.
when I use fireEvent.change(input, {target: {value: 'bob'}}
OnChange isn't triggered
When I use fireEvent.blur(input)
Blur is triggered
When I use simulate for react-dom/test-utils it works.

@kentcdodds
Copy link
Member

Hi @jamesBennett. Could you please open a new issue with a reproduction of your problem?

@markcellus
Copy link

@jamesBennett if you create an issue, can you come back and add a link to it here? I'd like to keep an eye out on this.

@TheNando
Copy link

TheNando commented Nov 14, 2019

@jamesBennett I was having this same issue and it turned out that it was the package jest-styled-components that was causing the input events to not trigger. Specifically, it was when we were importing it. If we included it during our setupJest.js, it would cause the issues with input events. But if we include it in the .spec file itself, it works.

@mainfraame
Copy link

I don't know if this helps anyone, but I noticed the fireChange event will only work if your input has a type assigned to it. eg.

<input type='text' onChange={onChange}/>

@jamesBennett
Copy link

#532

@jamesBennett
Copy link

@jamesBennett I was having this same issue and it turned out that it was the package jest-styled-components that was causing the input events to not trigger. Specifically, it was when we were importing it. If we included it during our setupJest.js, it would cause the issues with input events. But if we include it in the .spec file itself, it works.

I'm not using jest-styled-components

@kgwebsites
Copy link

kgwebsites commented Dec 31, 2019

I don't know if this helps anyone, but I noticed the fireChange event will only work if your input has a type assigned to it. eg.

<input type='text' onChange={onChange}/>

THANK YOU @mentierd

The input type has to be a valid html5 input type as well. If you have <input type="city" onChange={onChange} /> the onChange won't work in this case either.

@charlotte-bone
Copy link

charlotte-bone commented Feb 11, 2020

Just as an FYI in case this helps others. I wasn't able to get fireEvent to working, but ReactTestUtils.Simulate was working. It turns out it was because I was setting the document body in my jest setup

document.body = '<div id="app"></div>'

when I updated that to append the items I needed in the body it worked as expected.

const appContainer = document.createElement('div')
appContainer.setAttribute('id', 'app')
document.body.appendChild(appContainer)

So, double check you're not doing something to the document body :)

@jljorgenson18
Copy link

I'm seeing this issue as well using Karma on an actual browser. The input has a valid type attribute as well.

@kentcdodds
Copy link
Member

kentcdodds commented Feb 12, 2020

This issue is pretty old and general. I'm pretty confident that most of the people "experiencing issue" are in fact experiencing different issues entirely. Please open new issues (and please follow the issue template). Thanks!

@dusansen
Copy link

I had similar problem, I was testing some forms in Formik, and onChange event was not triggering when I change text input value using fireEvent from @testing-library/react. In the end I managed to do that just by calling jest.useFakeTimers() before everything and waiting for assertion.

it('should submit form on save click when all data is filled properly', async () => {
        const MOCKED_DATA = {
            name: 'name 1',
            description: 'desc 1'
        };
        const NEW_NAME = 'new name';
        const onSubmit = jest.fn();
        jest.useFakeTimers();
        const { queryByText, queryByPlaceholderText } = render(
            <MyForm
                initialValues={MOCKED_DATA}
                onSubmit={onSubmit}
            />
        );

        const nameInput = queryByPlaceholderText(NAME_PLACEHOLDER);
        fireEvent.change(nameInput, { target: { value: NEW_NAME } });
        fireEvent.click(queryByText('Save'));

        await waitFor(
            () => expect(onSubmit).toHaveBeenCalledWith({
                name: NEW_NAME,
                description: MOCKED_DATA.description
            })
        );
        jest.useRealTimers();
    });

@worc
Copy link

worc commented Oct 25, 2021

issue still exists in

"@testing-library/react": "^12.1.2",
"react": "^16.6.3",
"jest": "^26.6.3",

hard to say if it's just version mismatching or what. there seems to be a persistent issue over the last couple of years of onChange—or onClick—just not firing when testing input elements. the only pattern i've seen is folks talking about issues with jsdom not firing the right set of events at the right time, which would go with the whole version mismatch issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests