[Ivan Diaz] - FrontEnd - Add form validation, update names, focus first error on submit, unit testing updates [skip ci]
This commit is contained in:
parent
7b8012401e
commit
7c18179f07
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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'));
|
||||
|
|
Loading…
Reference in New Issue