@ngbrown/react-redux-form
Version:
Create Forms Easily with React and Redux
1,747 lines (1,412 loc) • 62.6 kB
JavaScript
/* eslint react/no-multi-comp:0 react/jsx-no-bind:0 */
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { assert } from 'chai';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { Provider, connect } from 'react-redux';
import TestUtils from 'react-dom/test-utils';
import capitalize from 'lodash/capitalize';
import mapValues from 'lodash/mapValues';
import _get from 'lodash.get';
import toPath from 'lodash.topath';
import sinon from 'sinon';
import createTestStore from 'redux-test-store';
import { testCreateStore, testRender } from './utils';
import Immutable from 'immutable';
import {
Field as _Field,
actions as _actions,
actionTypes,
modelReducer as _modelReducer,
formReducer as _formReducer,
controls,
} from '../src';
import {
Field as ImmutableField,
actions as immutableActions,
modelReducer as immutableModelReducer,
formReducer as immutableFormReducer,
} from '../immutable';
import isValid from '../src/form/is-valid';
const testContexts = {
standard: {
Field: _Field,
actions: _actions,
modelReducer: _modelReducer,
formReducer: _formReducer,
get: _get,
length: (value) => value.length,
},
immutable: {
Field: ImmutableField,
actions: immutableActions,
modelReducer: (model, initialState) =>
immutableModelReducer(model, Immutable.fromJS(initialState)),
formReducer: immutableFormReducer,
get: (value, path) => {
const result = value.getIn(toPath(path));
try {
return result.toJS();
} catch (e) {
return result;
}
},
length: (value) => value.size,
},
};
Object.keys(testContexts).forEach((testKey) => {
const testContext = testContexts[testKey];
const Field = testContext.Field;
const actions = testContext.actions;
const modelReducer = testContext.modelReducer;
const formReducer = testContext.formReducer;
const get = testContext.get;
const length = testContext.length;
describe('<Field /> component', () => {
const textFieldElements = [
['input', 'text'],
['input', 'password'],
['input', 'number'],
['input', 'color'],
['textarea'],
];
it('should wrap child components in a <div> if more than one', () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', { foo: 'bar' }),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<label />
<input />
</Field>
</Provider>
);
const div = TestUtils.findRenderedDOMComponentWithTag(field, 'div');
assert.ok(div);
assert.equal(
div.childNodes.length,
2);
});
it('should wrap child components in a <div> even if only one child', () => {
const store = testCreateStore({
test: modelReducer('test', { foo: 'bar' }),
testForm: formReducer('test'),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<input />
</Field>
</Provider>
);
assert.ok(TestUtils.findRenderedDOMComponentWithTag(field, 'div'));
assert.ok(TestUtils.findRenderedDOMComponentWithTag(field, 'input'));
});
it('should not wrap child components if only one child and null component', () => {
const store = testCreateStore({
test: modelReducer('test', { foo: 'bar' }),
testForm: formReducer('test'),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo" component={null}>
<input />
</Field>
</Provider>
);
assert.throws(() => TestUtils.findRenderedDOMComponentWithTag(field, 'div'));
assert.ok(TestUtils.findRenderedDOMComponentWithTag(field, 'input'));
});
it('should recursively handle nested control components', () => {
const store = testCreateStore({
test: modelReducer('test', { foo: 'bar' }),
testForm: formReducer('test'),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<div>
<label />
<input />
</div>
</Field>
</Provider>
);
const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
assert.equal(
control.value,
'bar',
'should set control to initial value');
control.value = 'testing';
TestUtils.Simulate.change(control);
assert.equal(
get(store.getState().test, 'foo'),
'testing',
'should update state when control is changed');
});
it('should handle nested control components created with React.Children.only', () => {
const store = testCreateStore({
test: modelReducer('test', { foo: 'bar' }),
testForm: formReducer('test'),
});
class ChildOnlyComp extends React.Component {
render() {
const child = React.Children.only(this.props.children);
return (
<div>
{child}
</div>
);
}
}
ChildOnlyComp.propTypes = {
children: PropTypes.node,
};
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<ChildOnlyComp>
<input />
</ChildOnlyComp>
</Field>
</Provider>
);
const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
assert.equal(
control.value,
'bar',
'should set control to initial value');
control.value = 'testing';
TestUtils.Simulate.change(control);
assert.equal(
get(store.getState().test, 'foo'),
'testing',
'should update state when control is changed');
});
it('should bypass null/falsey children', () => {
assert.doesNotThrow(() => {
const store = testCreateStore({
test: modelReducer('test', { foo: 'bar' }),
testForm: formReducer('test'),
});
TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<input />
<div>{false}</div>
</Field>
</Provider>
);
});
});
textFieldElements.forEach(([element, type]) => {
describe(`with <${element} ${type ? `type="${type}"` : ''}/>`, () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', { foo: 'bar' }),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
{React.createElement(element, { type })}
</Field>
</Provider>
);
const node = TestUtils.findRenderedDOMComponentWithTag(field, element);
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');
});
it('should have the appropriate type attribute', () => {
assert.equal(node.getAttribute('type'), type);
});
});
describe(`with a controlled <${element} ${type ? `type="${type}"` : ''} /> component`, () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', { foo: 'bar' }),
});
const TestField = connect(s => s)(props => {
const { test } = props;
return (
<Field model="test.foo">
{React.createElement(element, {
type,
value: get(test, 'foo'),
})}
</Field>
);
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<TestField />
</Provider>
);
const node = TestUtils.findRenderedDOMComponentWithTag(field, element);
it('should have the initial value of the state', () => {
assert.equal(
node.value,
'bar');
});
it('should update the value when the controlled input is changed', () => {
TestUtils.Simulate.change(node, {
target: { value: 'testing' },
});
assert.equal(
node.value,
'testing');
});
});
});
describe('with <input type="radio" />', () => {
const fields = {
deep: <Field model="test.foo">
<label>
<input type="radio" value="one" />
</label>
<label>
<input type="radio" value="two" />
</label>
</Field>,
shallow: <Field model="test.foo">
<input type="radio" value="one" />
<input type="radio" value="two" />
</Field>,
};
mapValues(fields, (fieldChild, key) => {
describe(`type: ${key}`, () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', { foo: 'two' }),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
{fieldChild}
</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 <input type="checkbox" /> (single toggle)', () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', {
single: true,
}),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.single">
<input type="checkbox" />
</Field>
</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');
TestUtils.Simulate.change(checkbox);
assert.equal(
get(store.getState().test, 'single'),
true, '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 <input type="checkbox" /> (multi toggle)', () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', {
foo: [1],
}),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo[]">
<input type="checkbox" value={1} />
<input type="checkbox" value={2} />
<input type="checkbox" value={3} />
</Field>
</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 <input type="checkbox" /> (custom onChange)', () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', {
foo: true,
}),
});
const handleOnChange = sinon.spy((e) => e);
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<input type="checkbox" onChange={handleOnChange} />
</Field>
</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 <input type="file" />', () => {
it('should update with an array of files', () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', { foo: [] }),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<input type="file" />
</Field>
</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 <select>', () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', {
foo: 'one',
}),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<select>
<option value="one" />
<option value="two" />
<option value="three" />
<optgroup>
<option value="four" />
<option value="five" />
<option value="six" />
</optgroup>
</select>
</Field>
</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('with <select> (defaultValue)', () => {
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', {
foo: undefined,
}),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo">
<select defaultValue="two">
<option value="one" />
<option value="two" />
<option value="three" />
<optgroup>
<option value="four" />
<option value="five" />
<option value="six" />
</optgroup>
</select>
</Field>
</Provider>
);
const select = TestUtils.findRenderedDOMComponentWithTag(field, 'select');
const options = TestUtils.scryRenderedDOMComponentsWithTag(field, 'option');
it('should select the option that matches the defaultValue attr'
+ 'if no initial value is provided', () => {
assert.isFalse(options[0].selected);
assert.isTrue(options[1].selected);
assert.equal(select.value, 'two');
});
it('the store should have the correct initial value', () => {
assert.equal(
get(store.getState().test, 'foo'),
'two');
});
});
describe('validators and validateOn property', () => {
const reducer = formReducer('test');
const store = testCreateStore({
testForm: reducer,
test: modelReducer('test', {
foo: '',
blur: '',
external: '',
}),
});
it('should set the proper field state for validation', () => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
validators={{
good: () => true,
bad: () => false,
custom: val => val !== 'invalid',
}}
>
<input type="text" />
</Field>
</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}>
<Field
model="test.blur"
validators={{
good: () => true,
bad: () => false,
custom: (val) => {
timesValidationCalled += 1;
return val !== 'invalid';
},
}}
validateOn="blur"
>
<input type="text" />
</Field>
</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}>
<Field
model="test.external"
validators={{
required: (val) => {
timesValidationCalled += 1;
return val && val.length;
},
}}
>
<input type="text" />
</Field>
</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');
});
it('should send the proper model value to the validators', () => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.items[]"
validators={{
required: (val) => val && length(val),
values: (val) => val,
}}
>
<input type="checkbox" value="first" />
<input type="checkbox" value="second" />
</Field>
</Provider>
);
const checkboxes = TestUtils.scryRenderedDOMComponentsWithTag(field, 'input');
assert.isFalse(store.getState().testForm.items.valid);
TestUtils.Simulate.change(checkboxes[0]);
assert.isTrue(store.getState().testForm.items.$form.valid);
assert.isTrue(
store.getState().testForm.items.$form.validity.required);
TestUtils.Simulate.change(checkboxes[1]);
assert.isTrue(
store.getState().testForm.items.$form.validity.required);
assert.isTrue(
store.getState().testForm.items.$form.validity.values);
TestUtils.Simulate.change(checkboxes[0]);
assert.isTrue(
store.getState().testForm.items.$form.validity.required);
assert.isTrue(
store.getState().testForm.items.$form.validity.values);
TestUtils.Simulate.change(checkboxes[1]);
assert.isFalse(
store.getState().testForm.items.$form.validity.required);
assert.isFalse(store.getState().testForm.items.$form.valid);
});
});
describe('asyncValidators and asyncValidateOn property', () => {
const reducer = formReducer('test');
const store = testCreateStore({
testForm: reducer,
test: modelReducer('test', {}),
});
it('should set the proper field state for a valid async validation', done => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
asyncValidators={{
testValid: (val, _done) => setTimeout(() => _done(true), 10),
}}
asyncValidateOn="blur"
>
<input type="text" />
</Field>
</Provider>
);
const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
const expectedStates = [
(state) => state.focus === false,
// initially valid
(state) => state.validating === true && isValid(state),
// true after validating
(state) => state.validating === false && isValid(state),
];
const actualStates = [];
store.subscribe(() => {
const state = store.getState();
actualStates.push(state.testForm.foo);
if (actualStates.length === expectedStates.length) {
expectedStates.map((expectedFn, i) =>
assert.ok(expectedFn(actualStates[i]), `${i}`)
);
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}>
<Field
model="test.foo"
asyncValidators={{
testValid: (val, _done) => setTimeout(() => _done(false), 10),
}}
asyncValidateOn="blur"
>
<input type="text" />
</Field>
</Provider>
);
const control = TestUtils.findRenderedDOMComponentWithTag(field, 'input');
const expectedStates = [
(state) => state.focus === false,
// initially valid
(state) => state.validating === true && isValid(state),
// false after validating
(state) => state.validating === false && !isValid(state),
];
const actualStates = [];
store.subscribe(() => {
const state = store.getState();
actualStates.push(state.testForm.foo);
if (actualStates.length === expectedStates.length) {
expectedStates.map((expectedFn, i) =>
assert.ok(expectedFn(actualStates[i]), `${i}`)
);
done();
}
});
TestUtils.Simulate.blur(control);
});
});
describe('sync and async validators', () => {
const reducer = formReducer('test');
const store = testCreateStore({
testForm: reducer,
test: modelReducer('test', {}),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
validators={{
required: (val) => val && val.length,
}}
asyncValidators={{
asyncValid: (_, asyncDone) => asyncDone(false),
}}
>
<input type="text" />
</Field>
</Provider>
);
const input = TestUtils.findRenderedDOMComponentWithTag(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 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('errors property', () => {
const reducer = formReducer('test');
it('should set the proper field state for errors', () => {
const store = testCreateStore({
testForm: reducer,
test: modelReducer('test', {
foo: '',
}),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
errors={{
length: (val) => val.length > 8 && 'too long',
valid: (val) => val !== 'valid' && 'not valid',
}}
>
<input type="text" />
</Field>
</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 store = testCreateStore({
testForm: reducer,
test: modelReducer('test', {
foo: '',
}),
});
let timesValidationCalled = 0;
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
errors={{
length: (val) => val.length > 8 && 'too long',
valid: (val) => {
timesValidationCalled += 1;
return val !== 'valid' && 'not valid';
},
}}
validateOn="blur"
>
<input type="text" required />
</Field>
</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 store = testCreateStore({
testForm: reducer,
test: modelReducer('test', {
foo: '',
}),
});
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
errors={(val) => !val && !val.length && 'Required'}
>
<input type="text" />
</Field>
</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', {}),
});
class DynamicSelectForm extends React.Component {
constructor() {
super();
this.state = { options: [1, 2] };
}
render() {
return (
<div>
<button onClick={() => this.setState({ options: [1, 2, 3] })} />
<Field model="test.foo" dynamic>
<select>
{this.state.options.map((option, i) =>
<option key={i} value={option} />
)}
</select>
</Field>
</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('wrapper components with component property', () => {
const store = testCreateStore({
test: modelReducer('test', {}),
testForm: formReducer('test'),
});
it('should wrap children with specified component (string)', () => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo" component="div">
<input type="text" />
</Field>
</Provider>
);
const wrapper = TestUtils.findRenderedDOMComponentWithTag(field, 'div');
assert.ok(wrapper);
});
it('should wrap children with specified component (class)', () => {
class Wrapper extends React.Component {
render() {
return <main className="wrapper">{this.props.children}</main>;
}
}
Wrapper.propTypes = { children: PropTypes.object };
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo" component={Wrapper}>
<input type="text" />
</Field>
</Provider>
);
const wrapper = TestUtils.findRenderedDOMComponentWithClass(field, 'wrapper');
assert.ok(wrapper);
});
it('should wrap children with specified component (function)', () => {
/* eslint-disable react/prop-types */
function Wrapper(props) {
return <section className="wrapper">{props.children}</section>;
}
/* eslint-enable react/prop-types */
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo" component={Wrapper}>
<input type="text" />
</Field>
</Provider>
);
const wrapper = TestUtils.findRenderedDOMComponentWithClass(field, 'wrapper');
assert.ok(wrapper);
});
it('should wrap children with a <div> when provided with className', () => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field model="test.foo" className="wrapper">
<input type="text" />
</Field>
</Provider>
);
const wrapper = TestUtils.findRenderedDOMComponentWithClass(field, 'wrapper');
assert.ok(wrapper);
});
});
describe('updateOn prop', () => {
const onEvents = [
'change',
'focus',
'blur',
];
onEvents.forEach((onEvent) => {
const store = testCreateStore({
test: modelReducer('test', { foo: 'initial' }),
testForm: formReducer('test'),
});
it(`should update the store when updateOn="${onEvent}"`, () => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
updateOn={onEvent}
>
<input type="text" />
</Field>
</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 reducer = formReducer('test');
const store = testCreateStore({
testForm: reducer,
test: modelReducer('test', {
foo: 'invalid',
}),
});
it('should always validate the model initially', () => {
TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
validators={{
initial: (val) => val !== 'invalid',
}}
>
<input type="text" />
</Field>
</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 reducer = modelReducer('test', { foo: '' });
const store = testCreateStore({
test: reducer,
testForm: formReducer('test'),
});
it('should change the model to the defaultValue on load', () => {
TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
>
<input type="text" defaultValue="testing" />
</Field>
</Provider>
);
assert.equal(
get(store.getState().test, 'foo'),
'testing');
});
});
describe('change on enter', () => {
const reducer = modelReducer('test');
const store = testCreateStore({
test: reducer,
testForm: formReducer('test'),
});
it('should change the model upon pressing Enter', () => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Field
model="test.foo"
updateOn="blur"
>
<input type="text" />
</Field>
</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');
});
});
describe('changeAction prop', () => {
const reducer = modelReducer('test', { foo: '' });
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}>
<Field
model="test.foo"
changeAction={(model, value) => {
customChanged = true;
return actions.change(model, value);
}}
>
<input type="text" />
</Field>
</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');
});
});
describe('event handlers on control', () => {
const reducer = modelReducer('test', { foo: '', bar: '' });
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}>
<Field
model="test.foo"
>
<input type="text" onChange={onChangeFnSpy} />
</Field>
</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>
<Field
model="test.foo"
>
<input type="text" onChange={onChangeFnSpy} />
</Field>
<Field
model="test.bar"
>
<input type="text" />
</Field>
</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>
<Field
model="test.foo"
>
<input type="text" onChange={onChangeFnFooSpy} />
</Field>
<Field
model="test.bar"
>
<input type="text" onChange={onChangeFnBarSpy} />
</Field>
</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}>
<Field
model="test.foo"
>
<input type="text" onChange={onChangeFnSpy} />
<div>{false}</div>
</Field>
</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}>
<Field
model="test.foo"
>
<input type="text" {...prop} />
</Field>
</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}`);
});
});
});
// TODO: control
it('should remove the item at the specified index of the array'
+ ' represented by the model', (done) => {
const initialState = {
foo: [
{ val: 1 },
{ val: 2 },
{ val: 3 },
],
};
/* eslint-disable global-require */
const store = createTestStore(createStore(combineReducers({
form: formReducer('test', initialState),
test: modelReducer('test', initialState),
}), applyMiddleware(require('redux-thunk').default)), done);
/* eslint-enable */
const index = 1;
TestUtils.renderIntoDocument(
<Provider store={store}>
<div>
<Field model="test.foo.0.val">
<div>
<label />
<input defaultValue="value" />
</div>
</Field>
<Field model="test.foo.1.val">
<div>
<label />
<input defaultValue="value" />
</div>
</Field>
<Field model="test.foo.2.val">
<div>
<label />
<input defaultValue="value" />
</div>
</Field>
</div>
</Provider>
);
assert.equal(get(store.getState().test, 'foo').length, 3);
store.when(actionTypes.CHANGE, (state) => {
assert.equal(get(state.test, 'foo').length, 2);
});
store.dispatch(actions.remove('test.foo', index));
});
// TODO: control
it('should maintain child references', (done) => {
const store = testCreateStore({
test: modelReducer('test', { foo: '' }