UNPKG

@ngbrown/react-redux-form

Version:

Create Forms Easily with React and Redux

1,669 lines (1,340 loc) 77.1 kB
/* eslint react/no-multi-comp:0 react/jsx-no-bind:0 */ import { assert } from 'chai'; import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-dom/test-utils'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import sinon from 'sinon'; import capitalize from '../src/utils/capitalize'; import _get from 'lodash.get'; import toPath from 'lodash.topath'; import i from 'icepick'; import Immutable from 'immutable'; import { testCreateStore, testRender } from './utils'; import { controls as _controls, modelReducer as _modelReducer, formReducer as _formReducer, Control as _Control, actions as _actions, combineForms as _combineForms, } from '../src'; import { controls as immutableControls, modelReducer as immutableModelReducer, formReducer as immutableFormReducer, Control as immutableControl, actions as immutableActions, combineForms as immutableCombineForms, } from '../immutable'; const testContexts = { standard: { controls: _controls, modelReducer: _modelReducer, formReducer: _formReducer, Control: _Control, actions: _actions, object: {}, get: _get, set: (state, path, value) => i.setIn(state, path, value), getInitialState: (state) => state, combineForms: _combineForms, }, immutable: { controls: immutableControls, modelReducer: immutableModelReducer, formReducer: immutableFormReducer, Control: immutableControl, actions: immutableActions, object: new Immutable.Map(), get: (value, path) => { const result = value.getIn(toPath(path)); try { return result.toJS(); } catch (e) { return result; } }, set: (state, path, value) => state.setIn(path, value), getInitialState: (state) => Immutable.fromJS(state), combineForms: immutableCombineForms, }, }; Object.keys(testContexts).forEach((testKey) => { const testContext = testContexts[testKey]; const controls = testContext.controls; const modelReducer = testContext.modelReducer; const formReducer = testContext.formReducer; const Control = testContext.Control; const actions = testContext.actions; const object = testContext.object; const get = testContext.get; const getInitialState = testContext.getInitialState; const combineForms = testContext.combineForms; describe(`<Control> component (${testKey} context)`, () => { describe('existence check', () => { it('should exist', () => { assert.ok(Control); }); }); describe('basic functionality', () => { const initialState = getInitialState({ foo: 'bar' }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const form = TestUtils.renderIntoDocument( <Provider store={store}> <Control model="test.foo" mapProps={controls.text} component="input" /> </Provider> ); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); it('should work as expected with a model (happy path)', () => { assert.ok(input); assert.equal(input.value, 'bar'); }); it('should handle changes properly', () => { input.value = 'new'; TestUtils.Simulate.change(input); assert.equal(get(store.getState().test, 'foo'), 'new'); }); }); describe('render prop', () => { const initialState = getInitialState({ foo: 'bar' }); const store = testCreateStore({ test: modelReducer('test', initialState), testForm: formReducer('test', initialState), }); const callback = sinon.spy(() => ( <div /> )); TestUtils.renderIntoDocument( <Provider store={store}> <Control model="test.foo" render={callback} /> </Provider> ); const renderPropObject = callback.args[0][0]; it('should call render callback with an object containing all relevant properties', () => { assert.equal(renderPropObject.viewValue, 'bar'); assert.equal(renderPropObject.modelValue, 'bar'); assert.deepEqual(renderPropObject.fieldValue, store.getState().testForm.foo); assert.property(renderPropObject, 'onFocus'); assert.property(renderPropObject, 'onBlur'); assert.property(renderPropObject, 'onChange'); assert.property(renderPropObject, 'onKeyPress'); }); }); describe('onLoad prop', () => { const initialState = getInitialState({ foo: 'bar' }); const store = testCreateStore({ test: modelReducer('test', initialState), testForm: formReducer('test', initialState), }); const handleLoad = sinon.spy(); const form = TestUtils.renderIntoDocument( <Provider store={store}> <Control model="test.foo" mapProps={controls.text} component="input" onLoad={handleLoad} /> </Provider> ); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); it('should call the onLoad function', () => { assert.ok(handleLoad.calledOnce); assert.equal(handleLoad.args[0][0], 'bar'); assert.containSubset(handleLoad.args[0][1], { initialValue: 'bar', }); assert.instanceOf(handleLoad.args[0][2], window.HTMLInputElement); assert.equal(handleLoad.args[0][2], input); }); }); }); describe('Extended Control components', () => { const textFieldElements = [ [''], ['text'], ['password'], ['text', 'email'], ['input', 'text'], ['input', 'password'], ['input', 'number'], ['input', 'color'], ['textarea'], ]; textFieldElements.forEach(([controlType, type]) => { describe(`with <Control.${controlType}> ${type ? `and type="${type}"` : ''}`, () => { const initialState = getInitialState({ foo: 'bar' }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const TestControl = Control[controlType] || Control; const field = TestUtils.renderIntoDocument( <Provider store={store}> <TestControl model="test.foo" type={type} /> </Provider> ); const node = TestUtils.findRenderedDOMComponentWithTag(field, controlType === 'textarea' ? 'textarea' : 'input'); it('should have an initial value from the model\'s initialState', () => { assert.equal( node.value, 'bar'); }); it('should dispatch a focus event when focused', () => { TestUtils.Simulate.focus(node); assert.containSubset( store.getState().testForm.foo, { focus: true }); }); it('should dispatch a blur event when blurred', () => { TestUtils.Simulate.blur(node); assert.containSubset( store.getState().testForm.foo, { focus: false }); }); it('should dispatch a change event when changed', () => { node.value = 'testing'; TestUtils.Simulate.change(node); assert.equal( get(store.getState().test, 'foo'), 'testing'); node.value = 'testing again'; TestUtils.Simulate.change(node); assert.equal( get(store.getState().test, 'foo'), 'testing again'); }); if ((controlType === 'text' || controlType === 'password') && !type) { it(`should have a type="${controlType}"`, () => { assert.equal(node.getAttribute('type'), controlType); }); } else { it('should have the appropriate type attribute', () => { assert.equal(node.getAttribute('type'), type); }); } }); }); describe('with <Control.radio />', () => { const initialState = getInitialState({ foo: 'two' }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <div> <Control.radio model="test.foo" value="one" /> <Control.radio model="test.foo" value="two" /> </div> </Provider> ); const [radioOne, radioTwo] = TestUtils.scryRenderedDOMComponentsWithTag(field, 'input'); it('should initially set the radio button matching the initial state to checked', () => { assert.equal(radioTwo.checked, true); assert.equal(radioOne.checked, false); }); it('should give each radio input a name attribute of the model', () => { assert.equal(radioOne.name, 'test.foo'); assert.equal(radioTwo.name, 'test.foo'); }); it('should dispatch a change event when changed', () => { TestUtils.Simulate.change(radioOne); assert.equal( get(store.getState().test, 'foo'), 'one'); TestUtils.Simulate.change(radioTwo); assert.equal( get(store.getState().test, 'foo'), 'two'); }); it('should check the appropriate radio button when model is externally changed', () => { store.dispatch(actions.change('test.foo', 'one')); assert.equal(radioOne.checked, true); assert.equal(radioTwo.checked, false); store.dispatch(actions.change('test.foo', 'two')); assert.equal(radioTwo.checked, true); assert.equal(radioOne.checked, false); }); it('should uncheck all radio buttons that are not equal to the value', () => { store.dispatch(actions.change('test.foo', 'three')); assert.equal(radioOne.checked, false); assert.equal(radioTwo.checked, false); }); }); describe('with <Control.checkbox /> (single toggle)', () => { const initialState = getInitialState({ single: true }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.checkbox model="test.single" /> </Provider> ); const checkbox = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); it('should initially set the checkbox to checked if the model is truthy', () => { assert.equal(checkbox.checked, true); }); it('should give each radio input a name attribute of the model', () => { assert.equal(checkbox.name, 'test.single'); }); it('should dispatch a change event when changed', () => { TestUtils.Simulate.change(checkbox); assert.equal( get(store.getState().test, 'single'), false, 'false'); assert.equal(checkbox.checked, false); TestUtils.Simulate.change(checkbox); assert.equal( get(store.getState().test, 'single'), true, 'true'); assert.equal(checkbox.checked, true); }); it('should check/uncheck the checkbox when model is externally changed', () => { store.dispatch(actions.change('test.single', true)); assert.equal(checkbox.checked, true); store.dispatch(actions.change('test.single', false)); assert.equal(checkbox.checked, false); }); it('should uncheck the checkbox for any falsey value', () => { store.dispatch(actions.change('test.single', '')); assert.equal(checkbox.checked, false); }); }); describe('with <Control.checkbox /> (single toggle, dynamic form, defaultChecked)', () => { const initialState = getInitialState({ single: true }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.checkbox model="test.other" defaultChecked /> </Provider> ); const checkbox = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); it('should initially set the checkbox to checked when defaultChecked is true', () => { assert.equal(checkbox.checked, true); }); it('should give each radio input a name attribute of the model', () => { assert.equal(checkbox.name, 'test.other'); }); it('should dispatch a change event when changed', () => { TestUtils.Simulate.change(checkbox); assert.equal( get(store.getState().test, 'other'), false, 'false'); assert.equal(checkbox.checked, false); TestUtils.Simulate.change(checkbox); assert.equal( get(store.getState().test, 'other'), true, 'true'); assert.equal(checkbox.checked, true); }); it('should check/uncheck the checkbox when model is externally changed', () => { store.dispatch(actions.change('test.other', true)); assert.equal(checkbox.checked, true); store.dispatch(actions.change('test.other', false)); assert.equal(checkbox.checked, false); }); it('should uncheck the checkbox for any falsey value', () => { store.dispatch(actions.change('test.other', '')); assert.equal(checkbox.checked, false); }); }); describe('with <Control.checkbox /> (single toggle, dynamic form, !defaultChecked)', () => { const initialState = getInitialState({ single: true }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.checkbox model="test.other" defaultChecked={false} /> </Provider> ); const checkbox = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); it('should initially set the checkbox to unchecked when defaultChecked is false', () => { assert.equal(checkbox.checked, false); }); it('should give each radio input a name attribute of the model', () => { assert.equal(checkbox.name, 'test.other'); }); it('should dispatch a change event when changed', () => { TestUtils.Simulate.change(checkbox); assert.equal( get(store.getState().test, 'other'), true, 'true'); TestUtils.Simulate.change(checkbox); assert.equal( get(store.getState().test, 'other'), false, 'false'); }); it('should check/uncheck the checkbox when model is externally changed', () => { store.dispatch(actions.change('test.other', false)); assert.equal(checkbox.checked, false); store.dispatch(actions.change('test.other', true)); assert.equal(checkbox.checked, true); }); it('should uncheck the checkbox for any falsey value', () => { store.dispatch(actions.change('test.other', '')); assert.equal(checkbox.checked, false); }); }); describe('with <Control.checkbox /> (multi toggle)', () => { const initialState = getInitialState({ foo: [1] }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <div> <Control.checkbox model="test.foo[]" value={1} /> <Control.checkbox model="test.foo[]" value={2} /> <Control.checkbox model="test.foo[]" value={3} /> </div> </Provider> ); const checkboxes = TestUtils.scryRenderedDOMComponentsWithTag(field, 'input'); it('should initially set the checkbox to checked if the model is truthy', () => { assert.equal(checkboxes[0].checked, true); }); it('should give each checkbox a name attribute of the model', () => { checkboxes.forEach(checkbox => { assert.equal(checkbox.name, 'test.foo[]'); }); }); it('should dispatch a change event when changed', () => { TestUtils.Simulate.change(checkboxes[0]); assert.sameMembers( get(store.getState().test, 'foo'), [], 'all unchecked'); TestUtils.Simulate.change(checkboxes[1]); assert.sameMembers( get(store.getState().test, 'foo'), [2], 'one checked'); TestUtils.Simulate.change(checkboxes[0]); assert.sameMembers( get(store.getState().test, 'foo'), [1, 2], 'two checked'); TestUtils.Simulate.change(checkboxes[2]); assert.sameMembers( get(store.getState().test, 'foo'), [1, 2, 3], 'all checked'); TestUtils.Simulate.change(checkboxes[0]); assert.sameMembers( get(store.getState().test, 'foo'), [2, 3], 'one unchecked'); }); it('should check the appropriate checkboxes when model is externally changed', () => { store.dispatch(actions.change('test.foo', [1, 2])); assert.isTrue(checkboxes[0].checked); assert.isTrue(checkboxes[1].checked); assert.isFalse(checkboxes[2].checked); }); }); describe('with <Control.checkbox /> (custom onChange)', () => { const initialState = getInitialState({ foo: true }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const handleOnChange = sinon.spy((e) => e); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.checkbox model="test.foo" onChange={handleOnChange} /> </Provider> ); const checkbox = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); TestUtils.Simulate.change(checkbox); it('should call the custom onChange event handler', () => { assert.ok(handleOnChange.calledOnce); }); it('should update the state as expected', () => { assert.isFalse(get(store.getState().test, 'foo')); }); }); describe('with <Control.file />', () => { it('should update with an array of files', () => { const initialState = getInitialState({ foo: [] }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.file model="test.foo" /> </Provider> ); const input = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); TestUtils.Simulate.change(input, { target: { type: 'file', files: [ { name: 'first.jpg' }, { name: 'second.jpg' }, ], }, }); assert.deepEqual( get(store.getState().test, 'foo'), [ { name: 'first.jpg' }, { name: 'second.jpg' }, ]); }); }); describe('with <Control.select />', () => { const initialState = getInitialState({ foo: 'one' }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.select model="test.foo"> <option value="one" /> <option value="two" /> <option value="three" /> <optgroup> <option value="four" /> <option value="five" /> <option value="six" /> </optgroup> </Control.select> </Provider> ); const select = TestUtils.findRenderedDOMComponentWithTag(field, 'select'); const options = TestUtils.scryRenderedDOMComponentsWithTag(field, 'option'); it('should select the option that matches the initial state of the model', () => { assert.isTrue(options[0].selected); assert.isFalse(options[1].selected); assert.equal(select.value, 'one'); }); it('should dispatch a change event when changed', () => { TestUtils.Simulate.change(options[1]); assert.equal( get(store.getState().test, 'foo'), 'two'); }); it('should select the appropriate <option> when model is externally changed', () => { store.dispatch(actions.change('test.foo', 'three')); assert.isTrue(options[2].selected); assert.equal(select.value, 'three'); }); it('should work with <optgroup>', () => { TestUtils.Simulate.change(options[3]); assert.isTrue(options[3].selected); assert.equal( get(store.getState().test, 'foo'), 'four'); }); }); describe('ignoring events with ignore prop', () => { const initialState = getInitialState({ foo: 'bar' }); const store = testCreateStore({ test: modelReducer('test', initialState), testForm: formReducer('test', initialState), }); const control = testRender( <Control.text model="test.foo" ignore={['focus', 'blur']} />, store); const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input'); it('ignores the events specified in the ignore prop', () => { assert.isFalse(store.getState().testForm.foo.focus); TestUtils.Simulate.focus(input); assert.isFalse(store.getState().testForm.foo.focus, 'focus event should be ignored'); TestUtils.Simulate.blur(input); assert.isFalse(store.getState().testForm.foo.touched, 'blur event should be ignored'); }); }); describe('validators and validateOn property', () => { const initialState = getInitialState({ foo: '', blur: '', external: '', }); const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test', initialState), }); it('should set the proper field state for validation', () => { const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" validators={{ good: () => true, bad: () => false, custom: val => val !== 'invalid', }} /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); control.value = 'valid'; TestUtils.Simulate.change(control); assert.deepEqual( store.getState().testForm.foo.errors, { good: false, bad: true, custom: false, }); control.value = 'invalid'; TestUtils.Simulate.change(control); assert.deepEqual( store.getState().testForm.foo.errors, { good: false, bad: true, custom: true, }); }); it('should validate on blur when validateOn prop is "blur"', () => { let timesValidationCalled = 0; const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.blur" validators={{ good: () => true, bad: () => false, custom: (val) => { timesValidationCalled += 1; return val !== 'invalid'; }, }} validateOn="blur" /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); control.value = 'valid'; assert.equal(timesValidationCalled, 1, 'validation should only be called once upon load'); TestUtils.Simulate.change(control); assert.equal(timesValidationCalled, 1, 'validation should not be called upon change'); TestUtils.Simulate.blur(control); assert.equal(timesValidationCalled, 2, 'validation should be called upon blur'); assert.deepEqual( store.getState().testForm.blur.errors, { good: false, bad: true, custom: false, }, 'should only validate upon blur'); }); it('should validate on external change', () => { let timesValidationCalled = 0; TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.external" validators={{ required: (val) => { timesValidationCalled += 1; return val && val.length; }, }} /> </Provider> ); assert.equal(timesValidationCalled, 1, 'validation called on load'); assert.isFalse(store.getState().testForm.external.valid); store.dispatch(actions.change('test.external', 'valid')); assert.isTrue(store.getState().testForm.external.valid); assert.equal(timesValidationCalled, 2, 'validation called because of external change'); }); }); describe('validateOn != updateOn', () => { const initialState = { foo: 'one', }; const radioStore = testCreateStore({ testForm: formReducer('test', initialState), test: modelReducer('test', initialState), }); const app = TestUtils.renderIntoDocument( <Provider store={radioStore}> <Control.radio model="test.foo" value="two" validateOn="focus" validators={{ isOne: (val) => val === 'one', }} /> </Provider> ); const input = TestUtils.findRenderedDOMComponentWithTag(app, 'input'); it('should initially be valid', () => { assert.isTrue(radioStore.getState().testForm.foo.valid); }); it('should still be valid after focusing', () => { TestUtils.Simulate.focus(input); assert.isTrue(radioStore.getState().testForm.foo.valid); assert.isTrue(radioStore.getState().testForm.foo.focus); }); }); describe('asyncValidators and asyncValidateOn property', () => { const reducer = formReducer('test'); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', object), }); it('should set the proper field state for a valid async validation', done => { const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" asyncValidators={{ testValid: (val, _done) => setTimeout(() => _done(true), 10), }} asyncValidateOn="blur" /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); const expectedStates = [ (state) => state.focus === false, // initially valid (state) => state.validating === true && state.valid, // true after validating (state) => state.validating === false && state.valid, ]; const actualStates = []; store.subscribe(() => { const state = store.getState(); actualStates.push(state.testForm.foo); if (actualStates.length === expectedStates.length) { expectedStates.map((expectedFn, index) => assert.ok(expectedFn(actualStates[index]), `${index}`) ); done(); } }); TestUtils.Simulate.blur(control); }); it('should set the proper field state for an invalid async validation', done => { const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" asyncValidators={{ testValid: (val, _done) => setTimeout(() => _done(false), 10), }} asyncValidateOn="blur" /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); const expectedStates = [ (state) => state.focus === false, // initially valid (state) => state.validating === true && state.valid, // false after validating (state) => state.validating === false && !state.valid, ]; const actualStates = []; store.subscribe(() => { const state = store.getState(); actualStates.push(state.testForm.foo); if (actualStates.length === expectedStates.length) { expectedStates.map((expectedFn, index) => assert.ok(expectedFn(actualStates[index]), `${index}`) ); done(); } }); TestUtils.Simulate.blur(control); }); }); describe('sync and async validators', () => { const reducer = formReducer('test'); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', object), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <div> <Control.text model="test.foo" validators={{ required: (val) => val && val.length, }} asyncValidators={{ asyncValid: (_, asyncDone) => asyncDone(false), }} /> <Control.text model="test.bar" validators={{ required: (val) => val && val.length, }} asyncValidators={{ asyncValid: (_, asyncDone) => asyncDone(false), }} validateOn="change" asyncValidateOn="change" /> </div> </Provider> ); const [input, concurrentInput] = TestUtils.scryRenderedDOMComponentsWithTag(field, 'input'); it('async validation should not run when field is invalid', () => { input.value = ''; TestUtils.Simulate.change(input); TestUtils.Simulate.blur(input); assert.deepEqual( store.getState().testForm.foo.validity, { required: false, }); assert.isUndefined(store.getState().testForm.foo.validity.asyncValid); }); it('async validation should run on invalid field ' + 'when concurrent with sync validation', () => { concurrentInput.value = ''; TestUtils.Simulate.change(concurrentInput); assert.deepEqual( store.getState().testForm.bar.validity, { required: false, asyncValid: false, }); assert.isDefined(store.getState().testForm.bar.validity.asyncValid); concurrentInput.value = 'changed'; TestUtils.Simulate.change(concurrentInput); assert.deepEqual( store.getState().testForm.bar.validity, { required: true, asyncValid: false, }); concurrentInput.value = ''; TestUtils.Simulate.change(concurrentInput); assert.deepEqual( store.getState().testForm.bar.validity, { required: false, asyncValid: false, }); }); it('async validation should not override sync validity', () => { input.value = 'asdf'; TestUtils.Simulate.change(input); TestUtils.Simulate.blur(input); assert.isDefined(store.getState().testForm.foo.validity.asyncValid); assert.deepEqual( store.getState().testForm.foo.validity, { required: true, asyncValid: false, }); }); }); describe('simultaneous sync and async validation', () => { it('should execute sync and async validation simultaneously' + ' if specified', () => { const initialState = getInitialState({ foo: '' }); const reducer = formReducer('test'); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', initialState), }); const syncValid = () => true; const asyncValid = () => true; const syncValidSpy = sinon.spy(syncValid); const asyncValidSpy = sinon.spy(asyncValid); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" validators={{ syncValidSpy }} validateOn="change" asyncValidators={{ asyncValidSpy }} asyncValidateOn="change" /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); control.value = 'testing'; TestUtils.Simulate.change(control); assert.isTrue(syncValidSpy.called); assert.isTrue(asyncValidSpy.called); }); it('should execute sync and async validation simultaneously' + ' if specified (with default validateOn)', () => { const initialState = getInitialState({ foo: '' }); const reducer = formReducer('test'); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', initialState), }); const syncValid = () => true; const asyncValid = () => true; const syncValidSpy = sinon.spy(syncValid); const asyncValidSpy = sinon.spy(asyncValid); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" validators={{ syncValidSpy }} asyncValidators={{ asyncValidSpy }} asyncValidateOn="change" /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); control.value = 'testing'; TestUtils.Simulate.change(control); assert.isTrue(syncValidSpy.called); assert.isTrue(asyncValidSpy.called); }); }); describe('validation after reset', () => { const initialState = getInitialState({ foo: '' }); const reducer = formReducer('test'); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', initialState), }); TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" validators={{ required: (val) => val && val.length, }} /> </Provider> ); it('should initially be invalid', () => { assert.deepEqual( store.getState().testForm.foo.validity, { required: false, }); }); it('should still be invalid after resetting the form model', () => { store.dispatch(actions.reset('test')); assert.deepEqual( store.getState().testForm.foo.validity, { required: false, }); }); it('should still be invalid after resetting the field model', () => { store.dispatch(actions.reset('test.foo')); assert.deepEqual( store.getState().testForm.foo.validity, { required: false, }); }); }); describe('initial value after reset', () => { const initialState = getInitialState({ foo: '' }); const store = testCreateStore({ testForm: formReducer('test', initialState), test: modelReducer('test', initialState), }); it('should reset the control to the last loaded value', () => { TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" /> </Provider> ); store.dispatch(actions.load('test.foo', 'new foo')); store.dispatch(actions.reset('test.foo')); assert.equal(get(store.getState().test, 'foo'), 'new foo'); }); const onEvents = [ 'change', 'focus', 'blur', ]; onEvents.forEach((updateOn) => { onEvents.forEach((validateOn) => { const condition = `updateOn="${updateOn}", validateOn="${validateOn}"`; it(`should clear reset intent when ${condition}`, () => { TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" updateOn={updateOn} validateOn={validateOn} /> </Provider> ); store.dispatch(actions.reset('test.foo')); const hasResetIntent = store.getState().testForm.foo.intents .some(intent => intent.type === 'reset'); assert.equal(hasResetIntent, false, 'has no pending reset intents'); }); }); }); }); describe('deep initial value after reset', () => { const initialState = getInitialState({ nest: { name: 'initial name', email: 'initial email', }, }); const store = createStore(combineForms({ user: initialState, })); TestUtils.renderIntoDocument( <Provider store={store}> <div> <Control.text model="user.nest.name" /> <Control.text type="email" model="user.nest.email" /> </div> </Provider> ); it('should reset the control to the last deeply loaded value', () => { store.dispatch(actions.load('user', getInitialState({ nest: { name: 'loaded name', email: 'loaded email', }, }))); store.dispatch(actions.reset('user.nest')); assert.equal(get(store.getState().user, 'nest.name'), 'loaded name'); assert.equal(get(store.getState().user, 'nest.email'), 'loaded email'); }); }); describe('errors property', () => { const reducer = formReducer('test'); it('should set the proper field state for errors', () => { const initialState = getInitialState({ foo: '' }); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" errors={{ length: (val) => val.length > 8 && 'too long', valid: (val) => val !== 'valid' && 'not valid', }} /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); control.value = 'valid'; TestUtils.Simulate.change(control); assert.deepEqual( store.getState().testForm.foo.errors, { length: false, valid: false, }); control.value = 'invalid string'; TestUtils.Simulate.change(control); assert.deepEqual( store.getState().testForm.foo.errors, { length: 'too long', valid: 'not valid', }); }); it('should only validate errors on blur if validateOn="blur"', () => { const initialState = getInitialState({ foo: '' }); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', initialState), }); let timesValidationCalled = 0; const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" errors={{ length: (val) => val.length > 8 && 'too long', valid: (val) => { timesValidationCalled += 1; return val !== 'valid' && 'not valid'; }, }} validateOn="blur" required /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); assert.equal(timesValidationCalled, 1, 'validation should be called on load'); control.value = 'valid'; TestUtils.Simulate.change(control); assert.equal(timesValidationCalled, 1, 'validation should not be called again on change'); TestUtils.Simulate.blur(control); assert.equal(timesValidationCalled, 2, 'validation should be called again on blur'); assert.deepEqual( store.getState().testForm.foo.errors, { length: false, valid: false, }); control.value = 'invalid string'; TestUtils.Simulate.change(control); assert.deepEqual( store.getState().testForm.foo.errors, { length: false, valid: false, }); TestUtils.Simulate.blur(control); assert.deepEqual( store.getState().testForm.foo.errors, { length: 'too long', valid: 'not valid', }); }); it('should handle a validator function for errors', () => { const initialState = getInitialState({ foo: '' }); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', initialState), }); const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" errors={(val) => !val && !val.length && 'Required'} /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); assert.equal( store.getState().testForm.foo.errors, 'Required'); control.value = 'valid'; TestUtils.Simulate.change(control); assert.deepEqual( store.getState().testForm.foo.errors, false); }); }); describe('dynamic components', () => { const reducer = formReducer('test'); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', object), }); class DynamicSelectForm extends React.Component { constructor() { super(); this.state = { options: [1, 2] }; } render() { return ( <div> <button onClick={() => this.setState({ options: [1, 2, 3] })} /> <Control.select model="test.foo" dynamic> {this.state.options.map((option, index) => <option key={index} value={option} /> )} </Control.select> </div> ); } } it('should properly update dynamic components inside <Field>', () => { const field = TestUtils.renderIntoDocument( <Provider store={store}> <DynamicSelectForm /> </Provider> ); let options = TestUtils.scryRenderedDOMComponentsWithTag(field, 'option'); const button = TestUtils.findRenderedDOMComponentWithTag(field, 'button'); assert.equal(options.length, 2); TestUtils.Simulate.click(button); options = TestUtils.scryRenderedDOMComponentsWithTag(field, 'option'); assert.equal(options.length, 3); }); }); describe('dynamic model', () => { const initialState = getInitialState({ foo: 'initial', bar: 'initial', }); const store = testCreateStore({ test: modelReducer('test', initialState), testForm: formReducer('test'), }); class DynamicModelControl extends React.Component { constructor() { super(); this.state = { model: 'foo' }; } render() { const { model } = this.state; return ( <div> <button onClick={() => this.setState({ model: 'bar' })} /> <Control.text model={`test.${model}`} /> </div> ); } } it('updates the proper field on change', () => { const field = TestUtils.renderIntoDocument( <Provider store={store}> <DynamicModelControl /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); const button = TestUtils.findRenderedDOMComponentWithTag(field, 'button'); control.value = 'value1'; TestUtils.Simulate.change(control); TestUtils.Simulate.click(button); control.value = 'value2'; TestUtils.Simulate.change(control); assert.equal(get(store.getState().test, 'foo'), 'value1'); assert.equal(get(store.getState().test, 'bar'), 'value2'); }); it('updates the proper form state of field on focus/blur', () => { const field = TestUtils.renderIntoDocument( <Provider store={store}> <DynamicModelControl /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); const button = TestUtils.findRenderedDOMComponentWithTag(field, 'button'); TestUtils.Simulate.focus(control); assert.equal(store.getState().testForm.foo.focus, true); assert.equal(store.getState().testForm.bar.focus, false); TestUtils.Simulate.blur(control); assert.equal(store.getState().testForm.foo.focus, false); assert.equal(store.getState().testForm.bar.focus, false); TestUtils.Simulate.click(button); TestUtils.Simulate.focus(control); assert.equal(store.getState().testForm.foo.focus, false, 'Second field should be focused instead, we switched the model'); assert.equal(store.getState().testForm.bar.focus, true); TestUtils.Simulate.blur(control); assert.equal(store.getState().testForm.foo.focus, false); assert.equal(store.getState().testForm.bar.focus, false); }); }); describe('updateOn prop', () => { const onEvents = [ 'change', 'focus', 'blur', ]; onEvents.forEach((onEvent) => { const initialState = getInitialState({ foo: 'initial' }); const store = testCreateStore({ test: modelReducer('test', initialState), testForm: formReducer('test'), }); it(`should update the store when updateOn="${onEvent}"`, () => { const field = TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" updateOn={onEvent} /> </Provider> ); const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input'); assert.equal(get(store.getState().test, 'foo'), 'initial'); const testValue = `${onEvent} test`; control.value = testValue; assert.equal(get(store.getState().test, 'foo'), 'initial', 'Model value should not change yet'); TestUtils.Simulate[onEvent](control); assert.equal(get(store.getState().test, 'foo'), testValue); }); }); }); describe('validation on load', () => { const initialState = getInitialState({ foo: 'invalid' }); const reducer = formReducer('test'); const store = testCreateStore({ testForm: reducer, test: modelReducer('test', initialState), }); it('should always validate the model initially', () => { TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" validators={{ initial: (val) => val !== 'invalid', }} /> </Provider> ); assert.containSubset( store.getState().testForm.foo, { validity: { initial: false, }, errors: { initial: true, }, }); assert.isFalse(store.getState().testForm.foo.valid); }); }); describe('syncing control defaultValue on load', () => { const initialState = getInitialState({ foo: '' }); const reducer = modelReducer('test', initialState); const store = testCreateStore({ test: reducer, testForm: formReducer('test'), }); it('should change the model to the defaultValue on load', () => { TestUtils.renderIntoDocument( <Provider store={store}> <Control.text model="test.foo" defaultValue="testing" /> </Provider> ); assert.equal( get(store.getState().test, 'foo'), 'testing'); }); }); describe('change on enter', () => { it('should change the model upon pressing Enter', () => { const reducer = modelReducer('test'); const store = testCreateStore({ test: reducer, testForm: formReducer('test'), }); const field = TestUtils.renderIntoDocument( <Provider store={store}>