redux-form / redux-form

A Higher Order Component using react-redux to keep form state in a Redux store
https://redux-form.com
MIT License
12.57k stars 1.63k forks source link

Testing a react-form, handleSubmit method stubbing #715

Closed andrewmclagan closed 7 years ago

andrewmclagan commented 8 years ago

Im struggling to find a solution when testing submit code within a hander. I'am currently exporting two variants of my form components as described in the react-redux documentation.

Component


export class LoginForm extends Component {

  static propTypes = {
    showForm: PropTypes.func.isRequired,
    login: PropTypes.func.isRequired,
    fields: PropTypes.object.isRequired,
    submitting: PropTypes.bool.isRequired,
    handleSubmit: PropTypes.func.isRequired,
    invalid: PropTypes.bool.isRequired,
    flashMessage: PropTypes.func.isRequired,
  }

  submit = () => {
    const params = [
      this.refs.login.value,
      this.refs.password.value,
    ];
    return this.props.login(...params).then(result => {
      this.props.flashMessage(result);
    }).catch(error => {
      this.props.flashMessage(error);
      return Promise.reject(error);
    });
  }

  render() {

    const { invalid, submitting, handleSubmit, showForm, fields: { login, password } } = this.props;

    return (
      <div>
        <h1 className="formTitle">Hello again</h1>

        <form className="login-form" onSubmit={handleSubmit(this.submit)}>
          <div className="form-group">
            <input type="text" ref="login" placeholder="Enter a login" className="form-control" {...login} />
            {login.touched && login.error && <div className="text-danger">{login.error}</div>}
          </div>

          <div className="form-group">
            <input type="password" ref="password" placeholder="Enter a password" className="form-control" {...password} />
            {password.touched && password.error && <div className="text-danger">{password.error}</div>}
          </div>

          <Button className="forgot-pass" bsStyle="link" onClick={showForm.bind(null, FORM_RECOVER)}>Forgot your password?</Button>
          <br />
          <Button type="submit" bsStyle="primary" disabled={submitting || invalid}>Log In</Button>
          <Button className="close" onClick={showForm.bind(null, FORM_HIDE)}>Close</Button>
          <hr />
          <Button className="sign-up" bsStyle="link" onClick={showForm.bind(null, FORM_SIGNUP)}>Dont have an account? Sign up</Button>
        </form>
      </div>
    );
  }
}

export default reduxForm({
  form: 'login',
  fields: ['login', 'password'],
  validate: loginValidation
}, { flashMessage })(LoginForm);

Test

import React from 'react';
import ReactDOM from 'react-dom';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
import { expect } from 'chai';

import { FORM_SIGNUP, FORM_RECOVER, FORM_HIDE } from 'redux/modules/auth';
import { Button } from 'react-bootstrap';
import { IsolatedLoginForm as LoginForm } from 'components';

describe('LoginForm - Unit Test', () => { 

  const field = { touched: null, error: null, value: '' };

  const properties = {
    showForm: () => {},
    login: () => {},
    fields: {
      login: field,
      password: field,
    },
    submitting: false,
    handleSubmit: () => {},
    invalid: false,
    flashMessage: () => {},
  };  

  it('it calls login on form submission', () => {
    properties.login = sinon.spy();
    const wrapper = shallow(
      <LoginForm {...properties} />
    );
    wrapper.find('Button[type="submit"]').simulate('click');
    expect(properties.login.calledOnce).to.be.true;  
  });    

});
erikras commented 8 years ago

Does your code work in your app, but just not in your test?

andrewmclagan commented 8 years ago

Sorry should have been clearer. The this.submit is never executed within the test. Although it may be due to enzyme / react-bootstrap. Im thinking that the event simulator in enzyme is not bubbling, and may need to be placed directly on the <a> element rather then the bootstrap component. will report if i find anything odd.

gregbarcza commented 8 years ago

+1 I'm using react-bootstrap. However if I specify onSubmit in the parent component it is working!

steakchaser commented 8 years ago

I think I ran into the same issue. I was able to work around it by grabbing an instance of the component, setting the stub, and then calling forceUpdate on the component AND calling update on the wrapper.

See: airbnb/enzyme/issues/586

gustavohenke commented 7 years ago

Im thinking that the event simulator in enzyme is not bubbling

This is correct. Enzyme's shallow( ... ).simulate() doesn't buble events, it just gets the event handler and invokes it directly. No real eventing going on.

Docs

palaniichukdmytro commented 6 years ago

It does not work for me. In my case onSubmit undefined, even when I passed the handleSubmit to props. Any idea how to simulate click with onSubmit={handleSubmit(this.update)

export class EditDevice extends Component {
    update = device => {
        console.log(device, 'device')
        if (device.miConfiguration)
            device.miConfiguration.isMiEnabled = device.miConfiguration.miConfigurationType !== MiConfigurationTypes.AccessPointOnly

        this.props.update(device).then(({success, ...error}) => {
            if (!success)
                throw new SubmissionError(error)

            this.returnToList()
        })}

    returnToList = () => this.props.history.push({pathname: '/setup/devices', state: {initialSkip: this.props.skip}})

    render = () => {
        let {isLoadingInProgress, handleSubmit, initialValues: {deviceType} = {}, change} = this.props

        const actions = [
            <Button
                name='cancel'
                onClick={this.returnToList}
            >
                <FormattedMessage id='common.cancel' />
            </Button>,
            <Button
                name='save'
                onClick={handleSubmit(this.update)}
                color='primary'
                style={{marginLeft: 20}}
            >
                <FormattedMessage id='common.save' />
            </Button>]

        return (
            <Page onSubmit={handleSubmit(this.update)} title={<FormattedMessage id='devices.deviceInfo' />} actions={actions} footer={actions}>
                <form >
                    {isLoadingInProgress && <LinearProgress mode='indeterminate'/>}
                    <div style={pageStyles.gridWrapper}>
                        <Grid container>
                            <Grid item xs={12}>
                                <Typography type='subheading'>
                                    <FormattedMessage id='devices.overview' />
                                </Typography>
                            </Grid>
                </form>
            </Page>
        )
    }
}
describe.only('EditPage', () => {

    let page, submitting, touched, error, reset, onSave, onSaveResponse, push
    let device = {miConfiguration:{isMiEnabled: 'dimon', miConfigurationType: 'Type'}}
    let update = sinon.stub().resolves({success: true})
    let handleSubmit = fn => fn(device)

    beforeEach(() => {
        submitting = false
        touched = false
        error = null
        reset = sinon.spy()
    })
    const props = {
        initialValues:{
            ...device,
        },
        update,
        handleSubmit,
        submitting: submitting,
        deviceId:999,
        history:{push: push = sinon.spy()},
        skip: skip,
        reset,
    }

    page = shallow(<EditDevice {...props}/>)

    it('should call push back to list on successful response', async () => {
        let update = sinon.stub().resolves({success: true})
        let device = {miConfiguration:{isMiEnabled: 'dimon', miConfigurationType: 'Type'}}

        page.find(Field).findWhere(x => x.props().name === 'name').simulate('change', {}, 'good name')
        await page.find(Page).props().footer.find(x => x.props.name === saveButtonName).props.onClick()
        push.calledOnce.should.be.true
        push.calledWith({pathname: '/setup/devices', state: {initialSkip: skip}}).should.be.true
    })
})
lock[bot] commented 5 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.