abidibo.net

Set material-ui TextField value for testing purposes

material-ui react redux sinon testing

Recently I began my trip in the world of react and redux. Everyone is hot with these "new" technologies, and after my previous experiences with angular I decided to give it a try.

In this entry I just want to share a tip about how to set the value of a material-ui TextField for testing purposes.

Scenario

I have a simple login form, with username and password fields, and a submit button. When the submit button is clicked, a function is called, dispatching an action which gets as arguments the username and password inputs values. Such values are computed using this.refs.FIELD_NAME.getValue() inside the parent component.

What follows is my code, the bold lines are the one of interest for this entry:

import React from 'react'
import { connect } from 'react-redux'
import { push } from 'react-router-redux'
import { async } from 'redux-api'
import TextField from 'material-ui/lib/text-field'
import RaisedButton from 'material-ui/lib/raised-button'
import AuthApi from 'api/Auth'

type Props = {
  auth: PropTypes.object.isRequired,
  onSubmit: PropTypes.func.isRequired,
}

export class Login extends React.Component {
  props: Props;

  login () {
    return () => {
      this.props.onSubmit(this.refs.user.getValue(), this.refs.password.getValue())
    }
  }

  render () {
    let error
    if (this.props.auth.error || this.props.auth.data.error) {
      let message = this.props.auth.error ? this.props.auth.error.message : this.props.auth.data.message
      error = <p className='error alert alert-danger'>{message}</p>
    } else {
      error = ''
    }

    return (
      <section className='login'>
        <h1>Login</h1>
        {error}
        <div className='form-group'>
          <TextField
            ref='user'
            hintText='Username'
          />
        </div>
        <div className='form-group'>
          <TextField
            type='password'
            ref='password'
            hintText='Password'
          />
        </div>
        <RaisedButton
          label='send'
          primary
          onMouseDown={this.login()}
        />
      </section>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    auth: state.auth
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    onSubmit: (user, password) => {
      let checkRedirect = (data) => {
        if (!data.error) {
          // must redirect
          console.log('login success -> redirect')
          dispatch(push('/'))
        }
        console.log('login error')
      }
      async(
        dispatch,
        (cb) => AuthApi.actions.login({user: user, password: password}, cb)
      ).then((data) => checkRedirect(data))
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Login)

There are some test to write here:

  • check that error messages are displayed when the API returns some sort of error
  • check that input fields are rendered
  • check that a submit button is rendered
  • check that submit button calls the onSubmit function
  • check that the onSubmit function is called with the right params

We'll discuss here about the last point, so we need a way to check if the onSubmit function was called with the right parameters. In order to do this, in our test we need to set the TextField values to some text.

I wonder you might be tempted to use the setValue method (there's always a setValue when a getValue method exists, don't you think?), well such method does not exist! At least in the last realeses. So how to set this value?

// assuming _rendered is the rendered parent component (using TestUtils)
// THIS WON'T WORK!
_rendered.refs.user.setValue('MYVALUE')
// THIS NEITHER!
ReactDOM.findDOMNode(_rendered.refs.user).value = 'MYVALUE'

The second attempt is near to the solution, the problem is that the TextField component contains many dom elements, not just an input, so we need to find this input first:

// find the input element inside TextField
let input_user = TestUtils.findRenderedDOMComponentWithTag(_rendered.refs.user, 'input')
// set its value
ReactDOM.findDOMNode(input_user).value = 'MYVALUE'

That's it! Now we can use sinon spies to check the arguments that the function received, and check them against the values we just set. Here comes the complete testing code:

/* eslint-disable no-unused-vars */
import React from 'react'
import ReactDOM from 'react-dom'
import { bindActionCreators } from 'redux'
import TestUtils from 'react-addons-test-utils'
import {Login} from 'containers/Login'
import RaisedButton from 'material-ui/lib/raised-button'

function shallowRender (component) {
  const renderer = TestUtils.createRenderer()

  renderer.render(component)
  return renderer.getRenderOutput()
}

function renderWithProps (props = {}) {
  return TestUtils.renderIntoDocument(
    <Login {...props} />
  )
}

function shallowRenderWithProps (props = {}) {
  return shallowRender(<Login {...props} />)
}

describe('(Component) Login', () => {
  let _component, _rendered, _props, _spies

  beforeEach(function () {
    _spies = {}
  })

  describe('Submit Button', () => {
    let _button
    beforeEach(function () {
      _props = {
        auth: {
          data: {
          }
        },
        onSubmit: (_spies.onSubmit = sinon.spy())
      }
      _rendered = renderWithProps(_props)
      _button = TestUtils.findRenderedComponentWithType(_rendered, RaisedButton)
    })

    it('Should exist.', function () {
      expect(_button).exist
    })

    it('Should have onMouseDown property.', function () {
      expect(_button.props.onMouseDown).exist
    })

    it('Should call login action.', function () {
      _spies.onSubmit.should.have.not.been.called
      _button.props.onMouseDown()
      _spies.onSubmit.should.have.been.called
    })

    it('Should call login action with right params.', function () {
      let input_user = TestUtils.findRenderedDOMComponentWithTag(_rendered.refs.user, 'input')
      let input_password = TestUtils.findRenderedDOMComponentWithTag(_rendered.refs.password, 'input')
      ReactDOM.findDOMNode(input_user).value = 'usr'
      ReactDOM.findDOMNode(input_password).value = 'pwd'
      _spies.onSubmit.should.have.not.been.called
      _button.props.onMouseDown()
      _spies.onSubmit.should.have.been.called
      // use eql instead of equal in order to check values and not reference!
      expect(_spies.onSubmit.getCalls()[0].args).eql(['usr', 'pwd'])
    })
  })
  // ...
})

That's quite easy indeed, but it cost me some time to get it working, that's why I share it with you. Have a good day!

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured