Testing onChange function in Jest
Asked Answered
E

6

40

I'm relatively new to Jest and testing in general. I have a component with an input element:

import * as React from "react";

export interface inputProps{
    placeholder: string;
    className: string;
    value: string;
    onSearch: (depID: string) => void;
}

onSearch(event: any){
    event.preventDefault();
    //the actual onclick event is in another Component
    this.props.onSearch(event.target.value.trim());
}

export class InputBox extends React.Component<inputProps, searchState> {
  render() {
        return (
            <input
                onChange={this.onSearch} //need to test this
                className={this.props.className} 
                type="text"
                value={this.props.value}
                placeholder={this.props.placeholder} />
        );
    }
}

I want a test that checks that input element's onChange is a function that takes in the input element's value attribute as the parameter. This is how far I have gotten so far:

//test to see the input element's onchange 
//returns a function that takes its value as a param
it("onChange param is the same value as the input value", () => {
    const mockFn = jest.fn();
    const input = enzyme.shallow(<InputBox 
                                    value="TestVal"
                                    placeholder="" 
                                    className="" 
                                    onSearch={mockFn}/>);


       input.find('input').simulate('change',  { preventDefault() {} });
       expect(mockFn.mock.calls).toBe("TestVal");
    });

I am going off of the first solution here Simulate a button click in Jest And: https://facebook.github.io/jest/docs/en/mock-functions.html

Edit: Running the above throws the following error:

 TypeError: Cannot read property 'value' of undefined
Edelweiss answered 10/1, 2018 at 4:51 Comment(1)
Might be a good idea to look into act as suggested in this questionGolda
V
45

Syntax on your code snippet I think should be:

import React from 'react';

export default class InputBox extends React.Component {
  onSearch(event) {
    event.preventDefault();
    this.props.onSearch(event.target.value.trim());
  }
  render () { return (<input onChange={this.onSearch.bind(this)} />); }
}

The test is failing because, as same you define the preventDefault function on the event object, you also must define other properties used on the onSearch function.

it('should call onChange prop', () => {
  const onSearchMock = jest.fn();
  const event = {
    preventDefault() {},
    target: { value: 'the-value' }
  };
  const component = enzyme.shallow(<InputBox onSearch={onSearchMock} />);
  component.find('input').simulate('change', event);
  expect(onSearchMock).toBeCalledWith('the-value');
});

Previous test code needs to define the event shape because you are using shallow rendering. If you want instead to test that the actual input value is being used on your onSearch function you need to try a full render with enzyme.mount:

it('should call onChange prop with input value', () => {
  const onSearchMock = jest.fn();
  const component = enzyme.mount(<InputBox onSearch={onSearchMock} value="custom value" />);
  component.find('input').simulate('change');
  expect(onSearchMock).toBeCalledWith('custom value');
});
Vertu answered 10/1, 2018 at 23:7 Comment(4)
You're welcome! @ZeroDarkThirty probably you can learn more stuff playing with this react-seed project.Vertu
Thanks! Btw, what is the significance of using mount instead of shallow in this case?Edelweiss
In this particular case, the main difference is that a mounted component will take care events creation for you, allowing to test that actual input value is used when calling the prop function callback (onSearch). In general, shallow rendering is used for real unit tests since no children components are rendered. And mount render is used for full DOM rendering which is ideal when you want to test the full component lifecycle... Check Full Rendering API hereVertu
my test fails, the received element is a full event object, while I expect just the string of the value... any help would be much appreciated 🙏Handbill
I
8

For those testing using TypeScript (and borrowing from the answers above), you'll need to perform a type coercion (as React.ChangeEvent<HTMLInputElement>) to ensure that the linter can view the signature as being compatible:

React file

export class InputBox extends React.Component<inputProps, searchState> {
  onSearch(event: React.ChangeEvent<HTMLInputElement>){
    event.preventDefault();
    //the actual onclick event is in another Component
    this.props.onSearch(event.target.value.trim());
  }

  render() {
    return (
      <input
        onChange={this.onSearch} //need to test this
        className={this.props.className} 
        type="text"
        value={this.props.value}
        placeholder={this.props.placeholder} />
      );
  }
}

Test file

it('should call onChange prop', () => {
  const onSearchMock = jest.fn();
  const event = {
    target: { value: 'the-value' }
  } as React.ChangeEvent<HTMLInputElement>;
  const component = enzyme.shallow(<InputBox onSearch={onSearchMock} />);
  component.find('input').simulate('change', event);
  expect(onSearchMock).toBeCalledWith('the-value');
});

or alternatively

it('should call onChange prop', () => {
  const onSearchMock = jest.fn();
  const event = {
    target: { value: 'the-value' }
  } as React.ChangeEvent<HTMLInputElement>;
  const component = enzyme.mount<InputBox>(<InputBox onSearch={onSearchMock} />);
  const instance = component.instance();
  instance.onSearch(event);
  expect(onSearchMock).toBeCalledWith('the-value');
});
Indigestion answered 25/2, 2020 at 21:53 Comment(0)
E
4

I figured out the solution.

So, instead of passing in the value inside InputBox, we have to pass it inside the second param of simulate as shown below. Then we simply check for equality against the first arg of the first call to the mockFn. Also, we can get rid of the event.preventDefault();

it("onChange param is the same value as the input element's value property", () => {
    const mockFn = jest.fn();
    const input = enzyme.shallow(<InputBox 
                                    value=""
                                    placeholder="" 
                                    className="" 
                                    onSearch={mockFn}/>);

    input.find('input').simulate('change', {target: {value: 'matched'} });
    expect(mockFn.mock.calls[0][0]).toBe('matched');
});
Edelweiss answered 10/1, 2018 at 23:2 Comment(0)
R
0

How about this one? I simulate the change event using enzyme and perform a snapshot test. Component

import React, { FunctionComponent, useState } from 'react';

const Index: FunctionComponent = () => {

  const [val, setVal] = useState('');

  const onInputChange = e => {
    e.preventDefault();
    setVal(e.target.value);
  };

  return (
    <input type='text' onChange={onInputChange} value={val} />
  );
};

export default Index;

Unit Test

describe('Index with enzyme', () => {
  it('Should set value to state when input is changed', () => {
    const container = shallow(<Index />);
    const input = container.find('input');
    input.simulate('change', { preventDefault: jest.fn, target: { value: "foo" } });
    expect(container).toMatchSnapshot();
  });
});

Snapshot

exports[`Index with enzyme Should set value to state when input is changed 1`] = `
  <input
    onChange={[Function]}
    type="text"
    value="foo"
  />
`;
Recreate answered 21/11, 2020 at 10:1 Comment(0)
F
0

I struggled with this for hours. Plus since I had multiple select fields on one page. What I found is that Textfield solution works differently from Select.test given on docs.

On the code I defined SelectProps with id. (You can also go with data-testid)

I could only trigger dropdown by clicking this field.

<TextField
  select
  variant = "outlined"
  value = { input.value || Number(0) }
  onChange = { value => input.onChange(value) }
  error = { Boolean(meta.touched && meta.error) }
  open = { open }
  SelectProps = {
    {
      id: `${input.name}-select`,
      MenuProps: {
        anchorOrigin: {
          vertical: "bottom",
          horizontal: "left"
        },
        transformOrigin: {
          vertical: "top",
          horizontal: "left"
        },
        getContentAnchorEl: null
      }
    }
  } 
  { ...props} >

  //yourOptions Goes here

 </TextField>

And in my test.

const pickUpAddress = document.getElementById("address-select");

UserEvent.click(pickUpAddress);
UserEvent.click(screen.getByTestId("address-select-option-0"));

Worked like a charm afterwards. Hope this helps.

Farver answered 20/1, 2021 at 18:8 Comment(0)
F
0

If you're writing lwc (salesforce) jest tests you can simulate this by selecting the input and dispatching an event.

const changeEvent = new CustomEvent('change', {
        detail: {
            'value': 'bad name'
        }
    });

element.shadowRoot.querySelector('lightning-input').dispatchEvent(changeEvent);
Fatty answered 10/6, 2022 at 10:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.