[Ivan Diaz] - FrontEnd - Add form validation, update names, focus first error on submit, unit testing updates [skip ci]

This commit is contained in:
Ivan Diaz 2016-06-05 17:09:42 -03:00
parent 7b8012401e
commit 7c18179f07
5 changed files with 226 additions and 109 deletions

View File

@ -1,57 +1,146 @@
const Form = require('core-components/form');
const Input = require('core-components/input');
// MOCKS
const ValidationFactoryMock = require('lib-app/__mocks__/validations/validation-factory-mock');
const Input = ReactMock();
// COMPONENTS
const Form = requireUnit('core-components/form', {
'lib-app/validations/validations-factory': ValidationFactoryMock,
'core-components/input': Input
});
describe('Form component', function () {
let form, inputs, onSubmit = stub();
let form, fields, onSubmit = stub();
beforeEach(function () {
function renderForm() {
form = TestUtils.renderIntoDocument(
<Form onSubmit={onSubmit}>
<div>
<Input name="first" value="value1"/>
<Input name="second" value="value2" />
<Input name="first" value="value1" required/>
<Input name="second" value="value2" required validation="CUSTOM"/>
</div>
<Input name="third" value="value3" />
</Form>
);
inputs = TestUtils.scryRenderedComponentsWithType(form, Input);
});
fields = TestUtils.scryRenderedComponentsWithType(form, Input);
}
it('should store input value in form state', function () {
expect(form.state.form).to.deep.equal({
first: 'value1',
second: 'value2',
third: 'value3'
function resetStubs() {
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub();
ValidationFactoryMock.validators.customValidatorMock.validate = stub();
ValidationFactoryMock.getValidator.reset();
onSubmit.reset();
}
describe('when mounting the form', function () {
beforeEach(renderForm);
afterEach(resetStubs);
it('should store fields values in form state', function () {
expect(form.state.form).to.deep.equal({
first: 'value1',
second: 'value2',
third: 'value3'
});
});
it('should set validations for required fields', function () {
expect(ValidationFactoryMock.getValidator).to.have.been.calledWith('DEFAULT');
expect(ValidationFactoryMock.getValidator).to.have.been.calledWith('CUSTOM');
expect(ValidationFactoryMock.getValidator).to.have.been.calledTwice;
expect(form.state.validations).to.deep.equal({
first: ValidationFactoryMock.validators.defaultValidatorMock,
second: ValidationFactoryMock.validators.customValidatorMock
});
});
});
it('should update form state if an input value changes', function () {
inputs[0].props.onChange({ target: {value: 'value4'}});
describe('when interacting with fields', function () {
beforeEach(renderForm);
afterEach(resetStubs);
expect(form.state.form).to.deep.equal({
first: 'value4',
second: 'value2',
third: 'value3'
it('should update form state if a field value changes', function () {
fields[0].props.onChange({ target: {value: 'value4'}});
expect(form.state.form).to.deep.equal({
first: 'value4',
second: 'value2',
third: 'value3'
});
});
it('should update field value if state value changes', function () {
form.setState({
form: {
first: 'value6',
second: 'value7',
third: 'value8'
}
});
expect(fields[0].props.value).to.equal('value6');
expect(fields[1].props.value).to.equal('value7');
expect(fields[2].props.value).to.equal('value8');
});
it('should validate required fields when blurring', function () {
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub().returns('MOCK_ERROR');
ValidationFactoryMock.validators.customValidatorMock.validate = stub().returns('MOCK_ERROR_2');
expect(fields[0].props.error).to.equal(undefined);
expect(fields[0].props.error).to.equal(undefined);
expect(fields[0].props.error).to.equal(undefined);
TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[0]));
expect(fields[0].props.error).to.equal('MOCK_ERROR');
TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[1]));
expect(fields[1].props.error).to.equal('MOCK_ERROR_2');
TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[2]));
expect(fields[2].props.error).to.equal(undefined);
});
});
it('should update input value if state value changes', function () {
form.setState({
form: {
first: 'value6',
second: 'value7',
third: 'value8'
}
describe('when submitting the form', function () {
beforeEach(renderForm);
afterEach(resetStubs);
it('should call onSubmit callback', function () {
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
expect(form.props.onSubmit).to.have.been.calledWith(form.state.form);
});
expect(inputs[0].props.value).to.equal('value6');
expect(inputs[1].props.value).to.equal('value7');
expect(inputs[2].props.value).to.equal('value8');
});
it('should validate all fields and not call onSubmit if there are errors', function () {
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub().returns('MOCK_ERROR');
ValidationFactoryMock.validators.customValidatorMock.validate = stub().returns('MOCK_ERROR_2');
fields[0].focus = spy(fields[0].focus);
fields[1].focus = spy(fields[1].focus);
it('should call onSubmit callback when form is submitted', function () {
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
expect(form.props.onSubmit).to.have.been.calledWith(form.state.form);
expect(fields[0].props.error).to.equal('MOCK_ERROR');
expect(fields[1].props.error).to.equal('MOCK_ERROR_2');
expect(fields[2].props.error).to.equal(undefined);
expect(form.props.onSubmit).to.not.have.been.called;
});
it('should focus the first field with error', function () {
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub().returns('MOCK_ERROR');
ValidationFactoryMock.validators.customValidatorMock.validate = stub().returns('MOCK_ERROR_2');
fields[0].focus = spy(fields[0].focus);
fields[1].focus = spy(fields[1].focus);
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
expect(fields[0].focus).to.have.been.called;
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub();
fields[0].focus.reset();
fields[1].focus.reset();
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
expect(fields[0].focus).to.not.have.been.called;
expect(fields[1].focus).to.have.been.called;
});
});
});

View File

@ -19,35 +19,13 @@ const Form = React.createClass({
},
componentDidMount() {
let formState = {};
let validations = {};
reactDFS(this.props.children, (child) => {
if (this.isValidInputType(child)) {
if (child.type === Input) {
formState[child.props.name] = child.props.value || '';
}
else if (child.type === Checkbox) {
formState[child.props.name] = child.props.checked || false;
}
if (child.props.required) {
validations[child.props.name] = ValidationFactory.getValidator(child.props.validation || 'DEFAULT');
}
}
});
this.setState({
form: formState,
validations: validations
});
this.setState(this.getInitialFormAndValidations());
},
render() {
return (
<form {...this.getProps()}>
{renderChildrenWithProps(this.props.children, this.getInputProps)}
{renderChildrenWithProps(this.props.children, this.getFieldProps)}
</form>
);
},
@ -60,54 +38,121 @@ const Form = React.createClass({
return props;
},
getInputProps({props, type}) {
getFieldProps({props, type}) {
let additionalProps = {};
if (type === Input || type === Checkbox) {
let inputName = props.name;
let fieldName = props.name;
additionalProps = {
ref: inputName,
value: this.state.form[inputName] || props.value,
error: this.state.errors[inputName],
onChange: this.handleInputChange.bind(this, inputName, type)
ref: fieldName,
value: this.state.form[fieldName] || props.value,
error: this.state.errors[fieldName],
onChange: this.handleFieldChange.bind(this, fieldName, type),
onBlur: this.validateField.bind(this, fieldName)
}
}
return additionalProps;
},
getFirstErrorField() {
let fieldName = _.findKey(this.state.errors);
let fieldNode;
if (fieldName) {
fieldNode = this.refs[fieldName];
}
return fieldNode;
},
getAllFieldErrors() {
let form = this.state.form;
let fields = Object.keys(this.state.form);
let errors = {};
_.each(fields, (fieldName) => {
errors = this.getErrorsWithValidatedField(fieldName, form, errors);
});
return errors;
},
getErrorsWithValidatedField(fieldName, form = this.state.form, errors = this.state.errors) {
let newErrors = _.clone(errors);
if (this.state.validations[fieldName]) {
newErrors[fieldName] = this.state.validations[fieldName].validate(form[fieldName], form);
}
return newErrors;
},
getInitialFormAndValidations() {
let form = {};
let validations = {};
reactDFS(this.props.children, (child) => {
if (this.isValidFieldType(child)) {
if (child.type === Input) {
form[child.props.name] = child.props.value || '';
}
else if (child.type === Checkbox) {
form[child.props.name] = child.props.checked || false;
}
if (child.props.required) {
validations[child.props.name] = ValidationFactory.getValidator(child.props.validation || 'DEFAULT');
}
}
});
return {
form: form,
validations: validations
}
},
handleSubmit(event) {
event.preventDefault();
if (this.hasFormErrors()) {
this.setState({
errors: this.validateAllFields()
errors: this.getAllFieldErrors()
}, this.focusFirstErrorField);
} else if (this.props.onSubmit) {
this.props.onSubmit(this.state.form);
}
},
handleInputChange(inputName, type, event) {
handleFieldChange(fieldName, type, event) {
let form = _.clone(this.state.form);
let errors;
form[inputName] = event.target.value;
form[fieldName] = event.target.value;
if (type === Checkbox) {
form[inputName] = event.target.checked || false;
form[fieldName] = event.target.checked || false;
}
errors = this.validateField(inputName, form);
this.setState({
form: form,
errors: errors
form: form
});
},
isValidFieldType(child) {
return child.type === Input || child.type === Checkbox;
},
hasFormErrors() {
return _.some(this.validateAllFields(), (error) => error);
return _.some(this.getAllFieldErrors());
},
validateField(fieldName) {
this.setState({
errors: this.getErrorsWithValidatedField(fieldName)
});
},
focusFirstErrorField() {
@ -116,43 +161,6 @@ const Form = React.createClass({
if (firstErrorField) {
firstErrorField.focus();
}
},
getFirstErrorField() {
let fieldName = _.findKey(this.state.errors);
let fieldNode;
if (fieldName) {
fieldNode = ReactDOM.findDOMNode(this.refs[fieldName]);
}
return fieldNode;
},
isValidInputType(child) {
return child.type === Input || child.type === Checkbox;
},
validateAllFields() {
let form = this.state.form;
let inputList = Object.keys(this.state.form);
let errors = {};
_.each(inputList, (inputName) => {
errors = this.validateField(inputName, form, errors);
});
return errors;
},
validateField(inputName, form = this.state.form, errors = this.state.errors) {
let newErrors = _.clone(errors);
if (this.state.validations[inputName]) {
newErrors[inputName] = this.state.validations[inputName].validate(form[inputName], form);
}
return newErrors;
}
});

View File

@ -34,6 +34,7 @@ const Input = React.createClass({
props.required = null;
props['aria-required'] = this.props.required;
props.type = (this.props.password) ? 'password' : 'text';
props.ref = 'nativeInput';
return props;
},
@ -47,6 +48,12 @@ const Input = React.createClass({
};
return classNames(classes);
},
focus() {
if (this.refs.nativeInput) {
this.refs.nativeInput.focus();
}
}
});

View File

@ -0,0 +1,12 @@
let customValidatorMock = {validate: stub()};
let defaultValidatorMock = {validate: stub()};
export default {
getValidator: spy(function (validation) {
return (validation === 'CUSTOM') ? customValidatorMock : defaultValidatorMock;
}),
validators: {
customValidatorMock: customValidatorMock,
defaultValidatorMock: defaultValidatorMock
}
};

View File

@ -11,6 +11,7 @@ global.chai = require('chai');
global.expect = chai.expect;
global.sinon = require('sinon');
global.stub = sinon.stub;
global.spy = sinon.spy;
global.proxyquire = require('proxyquire');
global.ReactMock = require('lib-test/react-mock');
chai.use(require('sinon-chai'));