content:  /* 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('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('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}>
              <Control.text
                model="test.foo"
                updateOn="blur"
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          control.value = 'testing';
  
          TestUtils.Simulate.keyPress(control, {
            key: 'Enter',
            keyCode: 13,
            which: 13,
          });
  
          assert.equal(
            get(store.getState().test, 'foo'),
            'testing');
        });
  
        it('should validate the model upon pressing Enter', () => {
          const reducer = modelReducer('test');
          const store = testCreateStore({
            test: reducer,
            testForm: formReducer('test'),
          });
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.text
                model="test.foo"
                updateOn="blur"
                validators={{
                  isNotBar: v => v !== 'bar',
                }}
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          control.value = 'bar';
  
          TestUtils.Simulate.keyPress(control, {
            key: 'Enter',
            keyCode: 13,
            which: 13,
          });
  
          assert.equal(
            get(store.getState().test, 'foo'),
            'bar');
  
          assert.deepEqual(
            store.getState().testForm.foo.errors,
            { isNotBar: true });
  
          assert.isFalse(store.getState().testForm.foo.valid);
        });
  
        it('should not change blur-updated textareas upon pressing Enter', () => {
          const reducer = modelReducer('test');
          const store = testCreateStore({
            test: reducer,
            testForm: formReducer('test'),
          });
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.textarea
                model="test.foo"
                updateOn="blur"
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'textarea');
  
          control.value = 'testing';
  
          TestUtils.Simulate.keyPress(control, {
            key: 'Enter',
            keyCode: 13,
            which: 13,
          });
  
          assert.isUndefined(
            get(store.getState().test, 'foo'));
        });
      });
  
      describe('handling onKeyPress', () => {
        const eventData = {
          key: 'a',
          keyCode: 65,
          which: 65,
        };
  
        const reducer = modelReducer('test');
        const store = testCreateStore({
          test: reducer,
          testForm: formReducer('test'),
        });
  
        it('should pass keyPress events to onKeyPress', (done) => {
          function handleKeyPress(event) {
            assert.containSubset(event, eventData);
            done();
          }
  
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.text
                model="test.foo"
                onKeyPress={handleKeyPress}
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          TestUtils.Simulate.keyPress(control, eventData);
        });
      });
  
      describe('changeAction prop', () => {
        const initialState = getInitialState({
          foo: '',
          checked: false,
        });
        const reducer = modelReducer('test', initialState);
        const store = testCreateStore({
          test: reducer,
          testForm: formReducer('test'),
        });
  
        it('should execute the custom change action', () => {
          let customChanged = false;
  
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.text
                model="test.foo"
                changeAction={(model, value) => {
                  customChanged = true;
                  return actions.change(model, value);
                }}
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          control.value = 'testing';
  
          TestUtils.Simulate.change(control);
  
          assert.isTrue(customChanged);
  
          assert.equal(
            get(store.getState().test, 'foo'),
            'testing');
        });
  
        it('should execute the custom change action (checkbox)', () => {
          let customChanged = false;
  
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.checkbox
                model="test.checked"
                changeAction={() => {
                  customChanged = true;
                }}
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          TestUtils.Simulate.change(control);
  
          assert.isTrue(customChanged);
        });
  
        it('should provide the inverse of the model value (checkbox)', (done) => {
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.checkbox
                model="test.customChecked"
                changeAction={(model, value) => {
                  assert.equal(model, 'test.customChecked');
                  assert.equal(value, true); // initial value is false
                  done();
                }}
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          TestUtils.Simulate.change(control);
        });
      });
  
      describe('event handlers on control', () => {
        const initialState = getInitialState({
          foo: '',
          bar: '',
        });
  
        const reducer = modelReducer('test', initialState);
        const store = testCreateStore({
          test: reducer,
          testForm: formReducer('test'),
        });
  
        it('should execute the custom change action', () => {
          const onChangeFn = (val) => val;
          const onChangeFnSpy = sinon.spy(onChangeFn);
  
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.text
                model="test.foo"
                onChange={onChangeFnSpy}
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          control.value = 'testing';
  
          TestUtils.Simulate.change(control);
  
          assert.isTrue(onChangeFnSpy.calledOnce);
          assert.isObject(onChangeFnSpy.returnValues[0]);
          assert.equal(
            onChangeFnSpy.returnValues[0].constructor.name,
            'SyntheticEvent');
          assert.equal(
            onChangeFnSpy.returnValues[0].target.value,
            'testing');
        });
  
        it('should not execute custom onChange functions of unchanged controls', () => {
          const onChangeFn = (val) => val;
          const onChangeFnSpy = sinon.spy(onChangeFn);
  
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <div>
                <Control.text
                  model="test.foo"
                  onChange={onChangeFnSpy}
                />
                <Control.text
                  model="test.bar"
                />
              </div>
            </Provider>
          );
  
          const [_, controlBar] = TestUtils.scryRenderedDOMComponentsWithTag(field, 'input');
  
          controlBar.value = 'testing';
  
          TestUtils.Simulate.change(controlBar);
  
          assert.isFalse(onChangeFnSpy.called);
        });
  
        it('should only execute custom onChange function pertaining to the changed input', () => {
          const onChangeFnFoo = (val) => val;
          const onChangeFnBar = (val) => val;
          const onChangeFnFooSpy = sinon.spy(onChangeFnFoo);
          const onChangeFnBarSpy = sinon.spy(onChangeFnBar);
  
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <div>
                <Control.text
                  model="test.foo"
                  onChange={onChangeFnFooSpy}
                />
                <Control.text
                  model="test.bar"
                  onChange={onChangeFnBarSpy}
                />
              </div>
            </Provider>
          );
  
          const [_, controlBar] = TestUtils.scryRenderedDOMComponentsWithTag(field, 'input');
  
          controlBar.value = 'testing';
  
          TestUtils.Simulate.change(controlBar);
  
          assert.isFalse(onChangeFnFooSpy.called);
          assert.isTrue(onChangeFnBarSpy.called);
        });
  
        it('should persist and return the event even when not returned', () => {
          const onChangeFn = () => {
          };
          const onChangeFnSpy = sinon.spy(onChangeFn);
  
          const field = TestUtils.renderIntoDocument(
            <Provider store={store}>
              <Control.text
                model="test.foo"
                onChange={onChangeFnSpy}
              />
            </Provider>
          );
  
          const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          control.value = 'testing 2';
  
          TestUtils.Simulate.change(control);
  
          assert.isTrue(onChangeFnSpy.calledOnce);
          assert.isUndefined(onChangeFnSpy.returnValues[0]);
          assert.equal(
            get(store.getState().test, 'foo'),
            'testing 2');
        });
  
        ['focus', 'blur'].forEach((event) => {
          const eventHandler = `on${capitalize(event)}`;
  
          it(`should execute the custom ${event} action`, () => {
            let targetValue;
  
            const onEvent = (e) => {
              targetValue = e.target.value;
  
              return e;
            };
  
            const onEventSpy = sinon.spy(onEvent);
  
            const prop = { [eventHandler]: onEventSpy };
  
            const field = TestUtils.renderIntoDocument(
              <Provider store={store}>
                <Control.text
                  model="test.foo"
                  {...prop}
                />
              </Provider>
            );
  
            const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
            control.value = `testing ${event}`;
  
            TestUtils.Simulate[event](control);
  
            assert.isTrue(onEventSpy.calledOnce);
            assert.isObject(onEventSpy.returnValues[0]);
            assert.equal(
              onEventSpy.returnValues[0].constructor.name,
              'SyntheticEvent');
  
            assert.equal(targetValue, `testing ${event}`);
          });
        });
      });
  
  
      describe('unmounting', () => {
        it('should set the validity of the model to true when umounted', () => {
          const initialState = getInitialState({ foo: '' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const container = document.createElement('div');
  
          const field = ReactDOM.render(
            <Provider store={store}>
              <Control.input model="test.foo" />
            </Provider>,
            container);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          store.dispatch(actions.setValidity('test.foo', false));
          assert.isFalse(store.getState().testForm.foo.valid);
  
          ReactDOM.unmountComponentAtNode(container);
  
          assert.isTrue(store.getState().testForm.foo.valid);
        });
  
        it('should only reset the validity of field-specific validators', () => {
          const initialState = getInitialState({ foo: '' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const container = document.createElement('div');
  
          const field = ReactDOM.render(
            <Provider store={store}>
              <Control.input
                model="test.foo"
                validators={{
                  internal: () => false,
                }}
              />
            </Provider>,
            container);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          assert.isFalse(store.getState().testForm.foo.valid);
  
          store.dispatch(actions.setValidity('test.foo', {
            ...store.getState().testForm.foo.validity,
            external: false,
          }));
  
          assert.isFalse(store.getState().testForm.foo.valid);
  
          ReactDOM.unmountComponentAtNode(container);
  
          assert.isFalse(store.getState().testForm.foo.valid);
  
          store.dispatch(actions.setValidity('test.foo', {
            ...store.getState().testForm.foo.validity,
            external: true,
          }));
  
          assert.isTrue(store.getState().testForm.foo.valid);
        });
  
        it('should not clobber other non-field-specific validators', () => {
          const initialState = getInitialState({});
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          store.dispatch(actions.setValidity('test.foo', {
            validator: false,
          }));
  
          const container = document.createElement('div');
  
          class WrappedControl extends React.Component {
            componentWillUnmount() {
              store.dispatch(actions.setErrors('test.foo', {}));
            }
  
            render() {
              return <Control.input model="test.foo" />;
            }
          }
  
          ReactDOM.render(
            <Provider store={store}>
              <WrappedControl />
            </Provider>,
            container);
  
          assert.isFalse(store.getState().testForm.foo.validity.validator);
  
          ReactDOM.unmountComponentAtNode(container);
  
          assert.isUndefined(store.getState().testForm.foo.validity.validator);
        });
      });
  
      describe('with <Control.reset>', () => {
        it('should reset the given model', () => {
          const initialState = getInitialState({ foo: '' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const container = document.createElement('div');
  
          const field = ReactDOM.render(
            <Provider store={store}>
              <div>
                <Control.input
                  model="test.foo"
                />
                <Control.reset model="test.foo" />
              </div>
            </Provider>,
            container);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
          const reset = TestUtils.findRenderedDOMComponentWithTag(field, 'button');
  
          input.value = 'changed';
  
          TestUtils.Simulate.change(input);
  
          assert.equal(get(store.getState().test, 'foo'), 'changed');
  
          TestUtils.Simulate.click(reset);
  
          assert.equal(get(store.getState().test, 'foo'), '');
        });
      });
  
      describe('with <Control.button>', () => {
        it('should exist', () => {
          assert.isFunction(Control.button);
        });
  
        const disabledProps = [
          true,
          { valid: false },
          (fieldValue) => !fieldValue.valid,
        ];
  
        disabledProps.forEach((disabled) => {
          it(`should be disabled with ${typeof disabled} as disabled prop value`, () => {
            const initialState = getInitialState({ foo: '' });
            const store = testCreateStore({
              testForm: formReducer('test', initialState),
            });
  
            const field = testRender(
              <Control.button model="test" disabled={disabled} />,
              store);
  
            const button = TestUtils.findRenderedDOMComponentWithTag(field, 'button');
  
            store.dispatch(actions.setValidity('test', false));
  
            assert.isTrue(button.disabled);
  
            if (disabled !== true) {
              store.dispatch(actions.setValidity('test', true));
  
              assert.isFalse(button.disabled);
            }
          });
        });
      });
  
      describe('manual focus/blur', () => {
        const initialState = getInitialState({ foo: 'bar' });
        const store = testCreateStore({
          test: modelReducer('test', initialState),
          testForm: formReducer('test', initialState),
        });
  
        const control = testRender(
          <Control.text
            model="test.foo"
          />, store);
  
        const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
  
        it('should manually focus the control', () => {
          store.dispatch(actions.focus('test.foo'));
  
          assert.equal(document.activeElement, input);
        });
  
        xit('should manually blur the control', () => {
          store.dispatch(actions.focus('test.foo'));
  
          assert.equal(document.activeElement, input);
  
          store.dispatch(actions.blur('test.foo'));
  
          assert.notEqual(document.activeElement, input);
        });
      });
  
      describe('handling on multiple events', () => {
        const initialState = getInitialState({ foo: 'bar' });
        const store = testCreateStore({
          test: modelReducer('test', initialState),
          testForm: formReducer('test', initialState),
        });
  
        const control = testRender(
          <Control.text
            model="test.foo"
            updateOn={['change', 'blur']}
          />, store);
  
        const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
  
        it('should update on change', () => {
          input.value = 'update on change';
  
          TestUtils.Simulate.change(input);
  
          assert.equal(get(store.getState().test, 'foo'), 'update on change');
        });
  
        it('should update on blur', () => {
          input.value = 'update on blur';
  
          TestUtils.Simulate.blur(input);
  
          assert.equal(get(store.getState().test, 'foo'), 'update on blur');
        });
      });
  
      describe('withField prop', () => {
        const initialState = getInitialState({ foo: 'bar' });
        const store = testCreateStore({
          test: modelReducer('test', initialState),
          testForm: formReducer('test', initialState),
        });
  
        [undefined, true].forEach((withField) => {
          it('should pass the fieldValue as the second argument to event'
            + ` handlers ${withField ? '' : 'by default'}`, (done) => {
            const handleChange = (_, fieldValue) => {
              assert.ok(fieldValue);
              assert.containSubset(fieldValue, {
                model: 'test.foo',
              });
              done();
            };
  
            const control = testRender(
              <Control.text
                model="test.foo"
                onChange={handleChange}
                withField={withField}
              />,
              store);
  
            const input = TestUtils.findRenderedDOMComponentWithTag(
              control, 'input');
  
            TestUtils.Simulate.change(input);
          });
        });
  
        it('should not pass fieldValue if withField = false', (done) => {
          const handleChange = (_, fieldValue) => {
            assert.isUndefined(fieldValue);
            done();
          };
  
          const control = testRender(
            <Control.text
              model="test.foo"
              onChange={handleChange}
              withField={false}
            />,
            store);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(
            control, 'input');
  
          TestUtils.Simulate.change(input);
        });
      });
  
      describe('debouncing', () => {
        it('should not update immediately on change', (done) => {
          const initialState = getInitialState({ foo: 'bar' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const control = testRender(
            <Control.text
              model="test.foo"
              debounce={10}
            />, store);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
          input.value = 'debounced';
  
          TestUtils.Simulate.change(input);
  
          assert.equal(get(store.getState().test, 'foo'), 'bar');
  
          setTimeout(() => {
            assert.equal(get(store.getState().test, 'foo'), 'debounced');
            done();
          }, 15); // 5ms buffer to eliminate race conditions
        });
  
        it('should always update viewValue immediately', () => {
          const initialState = getInitialState({ foo: 'bar' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const control = testRender(
            <Control.text
              model="test.foo"
              debounce={10}
            />, store);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
          input.value = 'debounced';
  
          TestUtils.Simulate.change(input);
  
          assert.equal(get(store.getState().test, 'foo'), 'bar');
          assert.equal(input.value, 'debounced');
        });
  
        it('should flush debounced changes when control is unmounted', () => {
          const initialState = getInitialState({ foo: 'bar' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const container = document.createElement('div');
  
          const control = ReactDOM.render(
            <Provider store={store}>
              <Control.text
                model="test.foo"
                debounce={1000}
              />
            </Provider>,
          container);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
          input.value = 'debounced';
  
          TestUtils.Simulate.change(input);
  
          ReactDOM.unmountComponentAtNode(container);
  
          assert.equal(get(store.getState().test, 'foo'), 'debounced');
          assert.equal(input.value, 'debounced');
        });
  
        it('should cancel debounced changes when control is reset', (done) => {
          const initialState = getInitialState({ foo: 'bar' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const control = testRender(
            <Control.text
              model="test.foo"
              debounce={10}
            />, store);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
          input.value = 'debounced';
  
          TestUtils.Simulate.change(input);
  
          store.dispatch(actions.reset('test'));
  
          setTimeout(() => {
            assert.equal(get(store.getState().test, 'foo'), 'bar');
            assert.equal(input.value, 'bar');
            done();
          }, 20);
        });
  
        it('should cancel debounced changes when control is reset then unmounted', (done) => {
          const initialState = getInitialState({ foo: 'bar' });
          const store = testCreateStore({
            test: modelReducer('test', initialState),
            testForm: formReducer('test', initialState),
          });
  
          const container = document.createElement('div');
  
          const control = ReactDOM.render(
            <Provider store={store}>
              <Control.text
                model="test.foo"
                debounce={10}
              />
            </Provider>,
          container);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
          input.value = 'debounced';
  
          TestUtils.Simulate.change(input);
  
          store.dispatch(actions.reset('test'));
          ReactDOM.unmountComponentAtNode(container);
  
          setTimeout(() => {
            assert.equal(get(store.getState().test, 'foo'), 'bar');
            assert.equal(input.value, 'bar');
            done();
          }, 20);
        });
      });
  
      describe('persist prop', () => {
        const initialState = getInitialState({ foo: 'bar' });
        const store = testCreateStore({
          test: modelReducer('test', initialState),
          testForm: formReducer('test', initialState),
        });
  
        it('should persist validation on unmount when persist = true', () => {
          const container = document.createElement('div');
  
          const field = ReactDOM.render(
            <Provider store={store}>
              <Control.input
                model="test.foo"
                validators={{ valid: () => false }}
                persist
              />
            </Provider>,
            container);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          ReactDOM.unmountComponentAtNode(container);
  
          assert.isFalse(store.getState().testForm.foo.valid);
          assert.deepEqual(store.getState().testForm.foo.validity, { valid: false });
        });
  
        it('should not persist validation on unmount when persist = false (default)', () => {
          const container = document.createElement('div');
  
          const field = ReactDOM.render(
            <Provider store={store}>
              <Control.input
                model="test.foo"
                validators={{ valid: () => false }}
                persist={false}
              />
            </Provider>,
            container);
  
          const input = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
  
          ReactDOM.unmountComponentAtNode(container);
  
          assert.isTrue(store.getState().testForm.foo.valid);
          assert.deepEqual(store.getState().testForm.foo.validity, {});
        });
      });
  
      describe('mixing controlProps and props', () => {
        const initialState = getInitialState({ foo: 'bar' });
        const store = testCreateStore({
          user: modelReducer('user', initialState),
          userForm: formReducer('user', initialState),
        });
  
        const control = testRender(
          <Control.button
            model="user"
            controlProps={{ className: 'button-class' }}
            lang="en"
          >
            Go
          </Control.button>, store);
  
        const button = TestUtils.findRenderedDOMComponentWithTag(control, 'button');
  
        it('should have controlProps on the rendered control', () => {
          assert.equal(button.getAttribute('class'), 'button-class');
        });
  
        it('should have normal, non-RRF-specific props passed to the rendered control', () => {
          assert.equal(button.getAttribute('lang'), 'en');
          assert.equal(button.textContent, 'Go');
        });
      });
  
      describe('non-string values for custom controls', () => {
        const initialState = getInitialState({
          extraData: { address: '221B Baker Street' },
        });
        const store = testCreateStore({
          user: modelReducer('user', initialState),
          userForm: formReducer('user', initialState),
        });
  
        /* eslint-disable react/prop-types */
        const ExtraData = ({ value }) => <div>{JSON.stringify(value)}</div>;
        /* eslint-enable */
  
        const control = testRender(
          <Control
            model="user.extraData"
            component={ExtraData}
            mapProps={{
              value: ({ modelValue }) => modelValue,
            }}
          />, store);
  
        const div = TestUtils.findRenderedDOMComponentWithTag(control, 'div');
  
        it('should render complex values correctly', () => {
          assert.equal(div.textContent, '{"address":"221B Baker Street"}');
        });
      });
    });
  });
  