Enzyme: Component State doesn't update on simulating input change event

Created on 20 Oct 2017  ·  7Comments  ·  Source: enzymejs/enzyme

Hello, I am new to Jest. I am testing a login page and I am simulating text change in my form. I am setting a value to the input and then I am triggering the change event. Everything works fine, the expected function is being called, the input box holds the value but the state of the component is not being updated. Here is part of related code:

Container:

export class LoginContainer extends Component {
  constructor() {
    super()

    this.state = {
      userName : '',
      password : ''
    }
  }

  /**
  * Set value of state in order to show in the input box
  *
  * @param {String} value    value to be set in the field
  * @param {String} propName field name
  */
  handleInputChange = (value, propName) => {
    this.setState({
      ...this.state,
      [propName] : value
    })
  }

  /**
   * Initialte login call on form submit
   *
   * @param {Object} event Event object
   */
  handleLogin = (event) => {
    event.preventDefault()
    const {userName, password} = this.state

    this.props.userLogin({
      userName,
      password
    })
      .then(() => {
        this.props.history.push('/')
      })
  }

  // ... some other class functions..

  render() {
    const { userName, password } = this.state
    const { isFetching, error } = this.props

    return (
      <Login 
        error={error}
        userName={userName}
        password={password}
        isFetching={isFetching}
        onFormSubmit={this.handleLogin}
        onInputChange={this.handleInputChange}
      />
    )
  }
}
... connecting the component with react-redux connect...

Component:

<form className={`center ${styles['login-form']}`}>
  <div className={styles['input-container']}>
    <input 
      type='text' 
      value={userName}
      placeholder='Enter Email' 
      onChange={(event) => onInputChange(event.target.value, 'userName')}
    />
  </div>
  <div className={styles['input-container']}>
    <input 
      type='password'
      value={password} 
      placeholder='Enter Password'
      onChange={(event) => onInputChange(event.target.value, 'password')}
    />
  </div>
  <div>
    <button
      type='submit'
      onClick={onFormSubmit}
      disabled={isFetching}
      className={`button ${styles.submit} ${isFetching && styles.disabled}`}
    >{isFetching ? 'Loading...' : 'Login'}</button>
  </div>
</form>

Test:

describe('>>> Login Container -- Shallow Rendering', () => {
  const initialState = userActionCreators.initialState
  let mountedScreen
  const loginScreen = (customState = {}) => {
    if (!mountedScreen) {
      const updatedState = {
        ...initialState,
        ...customState
      }

      mountedScreen = mount(<LoginContainer {...updatedState} />)
    }

    return mountedScreen
  }

  beforeEach(() => {
    mountedScreen = undefined
  })

  it('Simulates text change in input box', () => {
      const tree = loginScreen()
      const instance = tree.instance()
      const handleInputChangeSpy = jest.spyOn(instance, 'handleInputChange')
      const input = tree
        .find('input')
        .first()

      instance.forceUpdate()
      input.value = 'test'
      input.simulate('change', input)

      console.log('&&&&&&&&&&&&&&&&&&&&&')
      console.log(instance.state) // userName and password are empty here
      console.log('&&&&&&&&&&&&&&&&&&&&&')

      expect(handleInputChangeSpy).toHaveBeenCalledWith('test', 'userName')
    })
})

Console Error:

● >>> Login Container -- Shallow Rendering › Simulates text change in input box

    expect(jest.fn()).toHaveBeenCalledWith(expected)

    Expected mock function to have been called with:
      "test" as argument 1, but it was called with "".

Let me know if you need anything else.

Most helpful comment

  const usernameInput = wrapper.find("#username")
  usernameInput.instance().value = "correctUsername"
  usernameInput.simulate('change');

Works! :-) Please disregard the comment above.

@iiison I think this might actually work for you as well

All 7 comments

What versions of react, enzyme, etc?

Here is the link to package.json

@iiison your issue is that, because you're using class properties, the reference to handleInputChange is already stored in the enzyme wrapper before you spy on it.

Instead, change handleInputChange to be an instance method, and add this.handleInputChange = this.handleInputChange.bind(this) into your constructor - then you can spy on LoginContainer.prototype.handleInputChange before you create your enzyme wrapper.

@ljharb I have a similar problem.

handleChange(event) {
    const {name, value} = event.target;
}
(..)
 <input
className='form__control'
id='username'
type='text'
value={username}
placeholder='Username'
onChange={this.handleChange}
required
/>

In the test:

  const usernameInput = wrapper.find("#username")
  usernameInput.props().val = 'correctUsername'
  usernameInput.simulate('change');

I see the event in the handleChange but there is no name and value in them. It works in a browser. I'm a bit confused whether this is a bug, lack of feature, or intended behavior?
I can have this work doing

usernameInput.simulate('change', {target: {name: 'username', value: 'correctUsername'}});

But this seems error prone, especially if I wanted to generalize this event with page-object pattern.
Also if I just use the second argument of simulate then the actual value of the input is unchanged ,so if somewhere else I'm using refs to get value, that doesn't work.

You can see how it works in a browser when you type:
https://codesandbox.io/s/2jo8pqpl2r?expanddevtools=1

Thanks for a great work on this, help would be highly appreciated :-)

  const usernameInput = wrapper.find("#username")
  usernameInput.instance().value = "correctUsername"
  usernameInput.simulate('change');

Works! :-) Please disregard the comment above.

@iiison I think this might actually work for you as well

@Igandecki tried that but keep getting ShallowWrapper::instance() can only be called on the root

here is my code..

const input = MobileNumberComponent.find('input')
input.props().onChange({target: {
   id: 'mobile-no',
   value: '1234567900'
}});
MobileNumberComponent.update()
const Footer = (loginComponent.find('Footer'))
expect(Footer.find('Buttons').props().disabled).equals(false)

I have update my DOM with componentname.update()
And then checking submit button validation(disable/enable) with length 10 digit.

Was this page helpful?
0 / 5 - 0 ratings