Keith Jackson
Keith Jackson

Reputation: 3269

Testing React event handlers

Can someone please tell me how to test a React component event handler that looks like this...

handleChange(e) {
    this.setObjectState(e.target.getAttribute("for").toCamelCase(), e.target.value);
}

setObjectState(propertyName, value) {
    let obj = this.state.registrationData;
    obj[propertyName] = value;
    this.setState({registrationData: obj});
}

I've written a test using Enzyme to test the rendering and that will allow me to simulate the events to test that the handlers are actually called but that's the easy part. I want to test what happens when the event code runs. I can trigger these manually, but I don't know what to pass to the 'e' parameter in the test. If I use enzyme the test falls over unless I stub the event handler as 'e' is undefined.

Here's the enzyme test bits...

    describe("uses internal state to", () => {
        let stateStub = null;
        let formWrapper = null;

        beforeEach(() => {
            wrapper = shallow(<RegistrationForm />);
            instance = wrapper.instance();
            stateStub = sinon.stub(instance, 'setState');
            formWrapper = wrapper.find(Form.Wrapper);
        });

        afterEach(() => {
            stateStub.restore();
        });

        it("handle changing an email address", () => {
            formWrapper.find("[name='Email']").simulate('change')
            sinon.called(stateStub);
        })
    });

I briefly looked at using 'mount' instead of 'shallow' but I couldn't get that to run at all. That has many many issues, such as not being able to stub out things like the data load for the lookup drop downs before it tries to execute.

Here's what I'm trying to (ideally) do...

    describe("ALT uses internal state to", () => {
        let stateStub = null;
        let formWrapper = null;

        beforeEach(() => {
            wrapper = shallow(<RegistrationForm />);
            instance = wrapper.instance();
            stateStub = sinon.stub(instance, 'setState');
        });

        afterEach(() => {
            stateStub.restore();
        });

        it("handle changing an email address", () => {
            let e = 'some fake data - what IS this object?';
            instance.handleChange(e);
            sinon.called(stateStub);
            sinon.calledWith({registrationData: errr.. what?});
        })
    });

On request here's the full component code...

import ErrorProcessor from "./error-processor";
import Form from "../../../../SharedJs/components/form/index.jsx"
import HiddenState from "../../data/hidden-state"
import LookupRestServiceGateway from "../../../../SharedJs/data/lookup-rest-service-gateway"
import React from "react";
import RegistrationRestServiceGateway from "../../data/registration-rest-service-gateway"

export default class RegistrationForm extends React.Component {

    constructor(props) {
        super(props);
        this.state = props.defaultState || { 
            registrationData: {
                email: "",
                password: "",
                confirmPassword: "",
                firstName: "",
                lastName: "",
                employerID: null
            },
            registered: false,
            employersLookupData: []
        };

        this.formId = "registration-form";
        this.errorProcessor = new ErrorProcessor();
        this.employersDataSource = new LookupRestServiceGateway(`/api/lookups/employers/${HiddenState.getServiceOperatorCode()}`);
        this.registrationGateway = new RegistrationRestServiceGateway();

        this.handleChange = this.handleChange.bind(this);
        this.handleEmployerChange = this.handleEmployerChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleChange(e) {
        this.setObjectState(e.target.getAttribute("for").toCamelCase(), e.target.value);
    }

    handleEmployerChange(e) {
        this.setObjectState("EmployerID", e.target.value);
    }

    handleSubmit() {
        this.submitRegistration();
    }

    componentDidMount() {
        this.loadLookupData();
    }

    loadLookupData() {
        this.employersDataSource.getListItems({ successCallback: (data) => {
            this.setState({ employersLookupData: data ? data.items : [] });
        }});
    }

    setObjectState(propertyName, value) {
        let obj = this.state.registrationData;
        obj[propertyName] = value;
        this.setState({registrationData: obj});
    }

    submitRegistration() {
        this.registrationGateway.register({
            data: this.state.registrationData,
            successCallback: (data, status, xhr) => {
                this.setState({registered: true});
                if (data.errors && data.errors.length) {
                    this.errorProcessor.processErrorObject(this.formId, xhr);
                }
            },
            errorCallback: (xhr) => {
                this.errorProcessor.processErrorObject(this.formId, xhr);
            }
        });
    }

    render() {
        return (this.state.registered ? this.renderConfirmation() : this.renderForm());
    }

    renderConfirmation() {
        return (
            <div className = "registration-form">
                <p>Your registration has been submitted. An email will be sent to you to confirm your registration details before you can log in.</p>
                <Form.ErrorDisplay />
            </div>
        );
    }

    renderForm() {
        return (
            <Form.Wrapper formId = {this.formId}
                          className = "registration-form form-horizontal"
                          onSubmit = {this.handleSubmit}>
                <h4>Create a new account.</h4>
                <hr/>
                <Form.ErrorDisplay />
                <Form.Line name = "Email" 
                           label = "Email" 
                           type = "email"
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.email}
                           onChange = {this.handleChange} />
                <Form.Line name = "Password" 
                           label = "Password" 
                           type = "password"
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.password}
                           onChange = {this.handleChange} />
                <Form.Line name = "ConfirmPassword" 
                           label = "Confirm Password" 
                           type = "password"
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.confirmPassword}
                           onChange = {this.handleChange} />
                <Form.Line name = "FirstName" 
                           label = "First Name" 
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.firstName}
                           onChange = {this.handleChange} />
                <Form.Line name = "LastName" 
                           label = "Last Name" 
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.lastName}
                           onChange = {this.handleChange} />
                <Form.DropDownLine name = "EmployerID"
                                   label = "Employer"
                                   inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                                   emptySelection = "Please select an employer&hellip;"
                                   onChange = {this.handleEmployerChange}
                                   selectedValue = {this.state.registrationData.employerID}
                                   items = {this.state.employersLookupData}/>                                    
                <Form.Buttons.Wrapper className="col-sm-offset-3 col-md-offset-2 col-md-10 col-sm-9">
                    <Form.Buttons.Submit text = "Register"
                                         icon = "fa-user-plus" />
                </Form.Buttons.Wrapper>     
            </Form.Wrapper>                                                                                                                  
        );
    }
}

RegistrationForm.PropTypes = {
    defaultState: React.PropTypes.object
}

I've managed to get this working like this now but this feels very very rubbish - It gets me thinking that Enzyme's mount is the way to go but that introduces so many problems of its own and my spec file is already 10 times the size of my component and it all seems rather pointless at that level...

    describe("uses internal state to", () => {
        let stateStub = null;
        let formWrapper = null;

        beforeEach(() => {
            wrapper = shallow(<RegistrationForm />);
            instance = wrapper.instance();
            formWrapper = wrapper.find(Form.Wrapper);
            stateStub = sinon.stub(instance, 'setState');
        });

        afterEach(() => {
            stateStub.restore();
        });

        it("handle changing an email address", () => {
            $("body").append(`<input type="email" for="Email" id="field" class="form-control form-control " maxlength="10000" value="">`);
            let node = $("#field")[0];
            node.value = "[email protected]";
            instance.handleChange({target: node});
            sinon.assert.called(stateStub);
        })
    });

Upvotes: 4

Views: 11709

Answers (1)

Rafael Berro
Rafael Berro

Reputation: 2601

You can simulate an action to call the method and check the component state after the simulation, for example.

class Foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <div className={`clicks-${count}`}>
          {count} clicks
        </div>
        <a onClick={() => this.setState({ count: count + 1 })}>
          Increment
        </a>
      </div>
    );
  }
}

const wrapper = shallow(<Foo />);

expect(wrapper.find('.clicks-0').length).to.equal(1);
wrapper.find('a').simulate('click');
expect(wrapper.find('.clicks-1').length).to.equal(1);

If you are using Jest, try to follow this example from the official documentation.

Hope it helps.

Upvotes: 3

Related Questions