@ngbrown/react-redux-form
Version:
Create Forms Easily with React and Redux
1,735 lines (1,400 loc) • 67.1 kB
JavaScript
/* eslint react/no-multi-comp:0 react/jsx-no-bind:0 */
import { assert } from 'chai';
import React from 'react';
import PropTypes from 'prop-types';
import { createPortal, findDOMNode } from 'react-dom';
import TestUtils from 'react-dom/test-utils';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import createTestStore from 'redux-test-store';
import sinon from 'sinon';
import isValid from '../src/form/is-valid';
import { defaultTestContexts, testCreateStore, testRender } from './utils';
import {
Form as _Form,
modelReducer as _modelReducer,
formReducer as _formReducer,
Field as _Field,
Control as _Control,
actions as _actions,
actionTypes,
combineForms as _combineForms,
} from '../src';
import {
Form as ImmutableForm,
modelReducer as immutableModelReducer,
formReducer as immutableFormReducer,
Field as ImmutableField,
Control as ImmutableControl,
actions as immutableActions,
combineForms as immutableCombineForms,
} from '../immutable';
const testContexts = {
standard: {
...defaultTestContexts.standard,
Form: _Form,
modelReducer: _modelReducer,
formReducer: _formReducer,
Field: _Field,
Control: _Control,
actions: _actions,
combineForms: _combineForms,
},
immutable: {
...defaultTestContexts.immutable,
Form: ImmutableForm,
modelReducer: immutableModelReducer,
formReducer: immutableFormReducer,
Field: ImmutableField,
Control: ImmutableControl,
actions: immutableActions,
combineForms: immutableCombineForms,
},
};
Object.keys(testContexts).forEach((testKey) => {
const testContext = testContexts[testKey];
const Form = testContext.Form;
const modelReducer = testContext.modelReducer;
const formReducer = testContext.formReducer;
const Field = testContext.Field;
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(`<Form> component (${testKey} context)`, () => {
describe('wraps component if specified', () => {
const store = testCreateStore({
testForm: formReducer('test', object),
test: modelReducer('test'),
});
it('should wrap children with specified component (string)', () => {
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form model="test" component="section" />
</Provider>
);
const wrapper = TestUtils.findRenderedDOMComponentWithTag(form, 'section');
assert.ok(wrapper);
});
it('should wrap children with specified component (class)', () => {
class Wrapper extends React.Component {
render() {
const { children, ...other } = this.props;
return <form className="wrapper" {...other}>{children}</form>;
}
}
Wrapper.propTypes = {
children: PropTypes.object,
};
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form model="test" component={Wrapper} />
</Provider>
);
const wrapper = TestUtils.findRenderedDOMComponentWithClass(form, 'wrapper');
assert.ok(wrapper);
});
it('Renders as <form> by default', () => {
const field = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form model="test" />
</Provider>
);
const wrapper = TestUtils.findRenderedDOMComponentWithTag(field, 'form');
assert.ok(wrapper);
});
});
describe('validation on submit', () => {
function fixture() {
const initialState = getInitialState({ foo: '', bar: '' });
const store = testCreateStore({
testForm: formReducer('test', initialState),
test: modelReducer('test', initialState),
});
let timesValidated = 0;
function getTimesValidated() {
return timesValidated;
}
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
foo: (val) => {
timesValidated += 1;
return val === 'testing foo';
},
bar: {
one: (val) => val && val.length >= 1,
two: (val) => val && val.length >= 2,
},
}}
validateOn="submit"
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const [fooControl, barControl] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
return {
store,
form,
formElement,
fooControl,
barControl,
timesValidated: getTimesValidated,
};
}
it('should not validate before submit', () => {
const { timesValidated } = fixture();
assert.equal(timesValidated(), 0);
});
it('should not validate on change', () => {
const { fooControl, timesValidated } = fixture();
TestUtils.Simulate.change(fooControl);
assert.equal(timesValidated(), 0);
});
it('should validate all validators on submit', () => {
const {
formElement,
store,
timesValidated,
fooControl,
} = fixture();
assert.equal(timesValidated(), 0);
TestUtils.Simulate.submit(formElement);
assert.equal(timesValidated(), 1);
assert.isFalse(store.getState().testForm.foo.valid);
fooControl.value = 'testing foo';
TestUtils.Simulate.change(fooControl);
assert.equal(
get(store.getState().test, 'foo'),
'testing foo');
assert.equal(timesValidated(), 1,
'should not have validated again before submit');
TestUtils.Simulate.submit(formElement);
assert.equal(timesValidated(), 2);
assert.isTrue(store.getState().testForm.foo.valid);
});
it('should allow for keywise validation', () => {
const { formElement, store, barControl } = fixture();
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.bar,
{
errors: { one: true, two: true },
});
assert.isFalse(store.getState().testForm.bar.valid);
barControl.value = '1';
TestUtils.Simulate.change(barControl);
assert.equal(
get(store.getState().test, 'bar'),
'1');
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.bar,
{
errors: { one: false, two: true },
});
assert.isFalse(store.getState().testForm.bar.valid);
barControl.value = '12';
TestUtils.Simulate.change(barControl);
assert.equal(
get(store.getState().test, 'bar'),
'12');
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.bar,
{
errors: { one: false, two: false },
});
assert.isTrue(store.getState().testForm.bar.valid);
});
});
describe('error validation on submit', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
errors={{
foo: (val) => val !== 'valid foo' && 'invalid foo',
bar: {
one: (val) => val.length < 1 && 'bar too short',
two: (val) => val.length > 2 && 'bar too long',
},
}}
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const [fooControl, barControl] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
it('should validate all validators on submit', () => {
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.foo,
{
errors: 'invalid foo',
});
assert.isFalse(store.getState().testForm.foo.valid);
fooControl.value = 'valid foo';
TestUtils.Simulate.change(fooControl);
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.foo,
{
errors: false,
});
assert.isTrue(store.getState().testForm.foo.valid);
});
it('should allow for keywise validation', () => {
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.bar,
{
errors: {
one: 'bar too short',
two: false,
},
});
assert.isFalse(store.getState().testForm.bar.valid);
barControl.value = '1';
TestUtils.Simulate.change(barControl);
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.bar,
{
errors: {
one: false,
two: false,
},
});
assert.isTrue(store.getState().testForm.bar.valid);
barControl.value = '12';
TestUtils.Simulate.change(barControl);
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.bar,
{
errors: {
one: false,
two: false,
},
});
assert.isTrue(store.getState().testForm.bar.valid);
barControl.value = '123';
TestUtils.Simulate.change(barControl);
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.bar,
{
errors: {
one: false,
two: 'bar too long',
},
});
assert.isFalse(store.getState().testForm.bar.valid);
});
});
describe('submit validation with blur update fields', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test', initialState),
test: modelReducer('test', initialState),
});
const required = (v) => !!(v && v.length);
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
foo: required,
bar: required,
}}
validateOn="submit"
>
<Field model="test.foo" updateOn="blur">
<input type="text" />
</Field>
<Field model="test.bar" updateOn="blur">
<input type="text" />
</Field>
</Form>
</Provider>
);
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
const formNode = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
it('fields should not be validated initially', () => {
assert.ok(store.getState().testForm.foo.valid);
assert.ok(store.getState().testForm.bar.valid);
});
it('fields should not be validated after change', () => {
TestUtils.Simulate.change(inputs[0]);
TestUtils.Simulate.change(inputs[1]);
assert.ok(store.getState().testForm.foo.valid);
assert.ok(store.getState().testForm.bar.valid);
});
it('fields should be validated after submit', () => {
TestUtils.Simulate.submit(formNode);
assert.isFalse(store.getState().testForm.foo.valid);
assert.isFalse(store.getState().testForm.bar.valid);
});
it('fields should be validated with current values after submit', () => {
TestUtils.Simulate.submit(formNode);
// Not valid yet
assert.isFalse(store.getState().testForm.foo.valid);
assert.isFalse(store.getState().testForm.bar.valid);
inputs[0].value = 'first';
inputs[1].value = 'second';
TestUtils.Simulate.change(inputs[0]);
TestUtils.Simulate.change(inputs[1]);
TestUtils.Simulate.submit(formNode);
// Still not valid yet
assert.isFalse(store.getState().testForm.foo.valid);
assert.isFalse(store.getState().testForm.bar.valid);
// Change model
TestUtils.Simulate.blur(inputs[0]);
TestUtils.Simulate.blur(inputs[1]);
TestUtils.Simulate.submit(formNode);
// Should be valid
assert.ok(store.getState().testForm.foo.valid);
assert.ok(store.getState().testForm.bar.valid);
});
});
describe('error validation from silent changes on submit', () => {
const initialState = getInitialState({
foo: 'valid foo',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
errors={{
foo: (val) => val !== 'valid foo' && 'invalid foo',
bar: {
one: (val) => val.length < 1 && 'bar too short',
two: (val) => val.length > 2 && 'bar too long',
},
}}
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
it('should show correct initial error messages', () => {
assert.deepEqual(
store.getState().testForm.bar.errors,
{
one: 'bar too short',
two: false,
});
});
it('should validate errors upon submit after silent changes', () => {
store.dispatch(actions.load('test.foo', 'nope'));
TestUtils.Simulate.submit(formElement);
assert.equal(
store.getState().testForm.foo.errors,
'invalid foo');
});
});
describe('validation on change (default)', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
let timesBarValidationCalled = 0;
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
foo: (val) => val && val === 'testing foo',
bar: {
one: (val) => val && val.length >= 1,
two: (val) => val && val.length >= 2,
called: () => {
timesBarValidationCalled += 1;
return true;
},
},
}}
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
</Form>
</Provider>
);
const [fooControl] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
it('should validate form validators initially on load', () => {
assert.equal(timesBarValidationCalled, 1);
});
it('should validate form validators on field change', () => {
fooControl.value = 'invalid';
TestUtils.Simulate.change(fooControl);
assert.containSubset(
store.getState().testForm.foo,
{
errors: true,
});
assert.isFalse(store.getState().testForm.foo.valid);
fooControl.value = 'testing foo';
TestUtils.Simulate.change(fooControl);
assert.containSubset(
store.getState().testForm.foo,
{
errors: false,
});
assert.isTrue(store.getState().testForm.foo.valid);
});
it('should NOT run validation for fields that have not changed', () => {
fooControl.value = 'invalid';
TestUtils.Simulate.change(fooControl);
assert.equal(timesBarValidationCalled, 1);
});
});
describe('error validation on change', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
let timesBarValidationCalled = 0;
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
errors={{
foo: (val) => val !== 'valid foo' && 'invalid foo',
bar: () => {
timesBarValidationCalled += 1;
return 'bar validated';
},
}}
validateOn="change"
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
</Form>
</Provider>
);
const [fooControl] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
it('should validate form validators initially on load', () => {
assert.equal(timesBarValidationCalled, 1);
});
it('should validate form error validators on field change', () => {
fooControl.value = 'invalid';
TestUtils.Simulate.change(fooControl);
assert.containSubset(
store.getState().testForm.foo,
{
errors: 'invalid foo',
});
assert.isFalse(store.getState().testForm.foo.valid);
fooControl.value = 'valid foo';
TestUtils.Simulate.change(fooControl);
assert.containSubset(
store.getState().testForm.foo,
{
errors: false,
});
assert.isTrue(store.getState().testForm.foo.valid);
});
it('should NOT run validation for fields that have not changed', () => {
fooControl.value = 'testing';
TestUtils.Simulate.change(fooControl);
assert.equal(timesBarValidationCalled, 1);
});
});
describe('maintaining field validation state', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const required = (val) => val && val.length;
const store = testCreateStore({
testForm: formReducer('test', initialState),
test: modelReducer('test', initialState),
});
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
foo: required,
bar: required,
}}
validateOn="change"
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
</Form>
</Provider>
);
const [fooControl, barControl] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
it('should initially be invalid', () => {
assert.isFalse(store.getState().testForm.$form.valid);
});
it('should still be invalid if fields are still invalid', () => {
fooControl.value = 'valid';
TestUtils.Simulate.change(fooControl);
assert.isTrue(
store.getState().testForm.foo.valid,
'foo should be valid');
assert.isFalse(
store.getState().testForm.bar.valid,
'bar should be invalid');
assert.isFalse(
store.getState().testForm.$form.valid,
'form should be invalid');
});
it('should be valid once all fields are valid', () => {
fooControl.value = 'valid';
TestUtils.Simulate.change(fooControl);
barControl.value = 'valid';
TestUtils.Simulate.change(barControl);
assert.isTrue(
store.getState().testForm.foo.valid,
'foo should be valid');
assert.isTrue(
store.getState().testForm.bar.valid,
'bar should be valid');
assert.isTrue(
store.getState().testForm.$form.valid,
'form should be valid');
});
});
describe('onSubmit() prop', () => {
const initialState = getInitialState({
foo: '',
bar: '',
baz: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
let submitValue = null;
let submitEventValue = null;
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
foo: (val) => val && val === 'valid',
baz: {
validationKey: (val) => val && val === 'valid',
},
}}
errors={{
bar: (val) => val !== 'bar' && 'bar invalid',
}}
onSubmit={(val, event) => {
submitValue = val;
submitEventValue = event;
return true;
}}
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
<Field model="test.baz">
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const [fooControl, barControl, bazControl] = TestUtils
.scryRenderedDOMComponentsWithTag(form, 'input');
it('should NOT call onSubmit if form is invalid', () => {
TestUtils.Simulate.submit(formElement);
assert.isNull(submitValue);
fooControl.value = 'valid';
TestUtils.Simulate.change(fooControl);
assert.isTrue(
store.getState().testForm.foo.valid);
assert.isNull(submitValue);
assert.isNull(submitEventValue);
});
it('should set submitFailed to true if form is invalid and submitted', () => {
TestUtils.Simulate.submit(formElement);
assert.isTrue(store.getState().testForm.$form.submitFailed);
});
it('should call onSubmit with model value and event if form is valid', () => {
barControl.value = 'bar';
TestUtils.Simulate.change(barControl);
TestUtils.Simulate.submit(formElement);
assert.isNull(submitValue,
'should not be valid yet because baz is still invalid');
bazControl.value = 'valid';
TestUtils.Simulate.change(bazControl);
assert.deepEqual(
get(store.getState().test),
{
foo: 'valid',
bar: 'bar',
baz: 'valid',
});
TestUtils.Simulate.submit(formElement);
assert.deepEqual(
get(submitValue),
{
bar: 'bar',
baz: 'valid',
foo: 'valid',
});
assert.instanceOf(submitEventValue.target, window.HTMLElement,
'second argument should be event with a target');
});
});
describe('onSubmit() mixing form and field validation', () => {
it('should NOT call onSubmit if any form subfield is invalid', () => {
const initialState = getInitialState({
foo: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
let submitValue = null;
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
'': () => true,
}}
onSubmit={(val) => {
submitValue = val;
return true;
}}
>
<Field
model="test.foo"
validators={{ fieldLevel: () => false }}
>
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
TestUtils.Simulate.submit(formElement);
assert.isNull(submitValue);
assert.isFalse(store.getState().testForm.$form.submitted);
});
});
describe('validation of form itself', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
'': {
foobar: (val) => get(val, 'foo') + get(val, 'bar') === 'foobar',
},
}}
errors={{
'': {
formError: (val) => get(val, 'foo') === 'error' && 'form error',
},
}}
>
<Field model="test.foo">
<input type="text" />
</Field>
<Field model="test.bar">
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const [fooControl, barControl] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
it('should be able to set keyed validation to the form model', () => {
TestUtils.Simulate.submit(formElement);
assert.isFalse(store.getState().testForm.$form.valid);
fooControl.value = 'foo';
TestUtils.Simulate.change(fooControl);
barControl.value = 'bar';
TestUtils.Simulate.change(barControl);
TestUtils.Simulate.submit(formElement);
assert.isTrue(store.getState().testForm.$form.valid);
});
it('should be able to set keyed errors to the form model', () => {
fooControl.value = 'error';
TestUtils.Simulate.change(fooControl);
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.$form,
{
errors: {
foobar: true,
formError: 'form error',
},
});
assert.isFalse(store.getState().testForm.$form.valid);
});
});
describe('external validators', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
let timesSubmitCalled = 0;
function handleSubmit() {
timesSubmitCalled++;
}
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
onSubmit={handleSubmit}
>
<Field
model="test.foo"
validators={{
required: (val) => val && val.length > 5,
}}
>
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const inputElement = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
it('should initially be invalid if the form state is invalid', () => {
TestUtils.Simulate.submit(formElement);
assert.equal(timesSubmitCalled, 0);
});
it('should prevent onSubmit if the form state is invalid after change', () => {
inputElement.value = 'short';
TestUtils.Simulate.change(inputElement);
TestUtils.Simulate.submit(formElement);
assert.equal(timesSubmitCalled, 0);
});
it('should submit once the form state is valid after change', () => {
inputElement.value = 'longer';
TestUtils.Simulate.change(inputElement);
TestUtils.Simulate.submit(formElement);
assert.equal(timesSubmitCalled, 1);
});
});
describe('deep state path', () => {
const initialState = getInitialState({
foo: 'deep foo',
});
const formsReducer = combineReducers({
testForm: formReducer('forms.test'),
test: modelReducer('forms.test', initialState),
});
const store = testCreateStore({
forms: formsReducer,
});
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="forms.test"
>
<Control model=".foo" />
</Form>
</Provider>
);
const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
it('should resolve the model value', () => {
assert.equal(input.value, 'deep foo');
});
});
describe('invalidating async validity on form change', () => {
const initialState = getInitialState({ val: 'invalid' });
const store = createTestStore(testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
}));
function handleSubmit() {
const promise = new Promise((resolve, reject) => reject('Form is invalid'));
store.dispatch(actions.submit('test', promise));
}
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
onSubmit={handleSubmit}
>
<Field model="test.foo">
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const inputElement = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
it('should set errors from rejected submit handler on valid submit', (done) => {
store.when(actionTypes.SET_ERRORS, (state) => {
assert.isFalse(isValid(state.testForm));
assert.equal(state.testForm.$form.errors, 'Form is invalid');
done();
});
TestUtils.Simulate.submit(formElement);
});
it('should set validity on form changes after submit failed', () => {
inputElement.value = 'valid';
TestUtils.Simulate.change(inputElement);
assert.isTrue(store.getState().testForm.$form.valid);
});
});
describe('invalidating async validity on form change with form validators', () => {
const initialState = getInitialState({ foo: 'invalid' });
const store = createTestStore(testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
}));
function handleSubmit() {
store.dispatch(actions.batch('test', [
actions.setSubmitFailed('test'),
actions.setErrors('test', 'Form is invalid', { errors: true }),
]));
}
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
foo: (val) => val && val.length,
}}
onSubmit={handleSubmit}
>
<Field model="test.foo">
<input type="text" />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const inputElement = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
it('should set errors from rejected submit handler on valid submit', () => {
assert.isTrue(store.getState().testForm.$form.valid);
TestUtils.Simulate.submit(formElement);
assert.containSubset(
store.getState().testForm.$form,
{ errors: 'Form is invalid' });
});
it('should set validity on form changes after submit failed', () => {
inputElement.value = 'valid';
TestUtils.Simulate.change(inputElement);
assert.isTrue(store.getState().testForm.$form.valid);
});
});
describe('submit after fields valid but form invalid', () => {
const handleSubmit = sinon.spy((val) => val);
const initialState = getInitialState({
pass1: '',
pass2: '',
});
const store = createTestStore(testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
}));
const passwordsMatch = (val) => get(val, 'pass1') === get(val, 'pass2');
const required = (val) => val && val.length;
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
'': [passwordsMatch],
pass1: [required],
pass2: [required],
}}
onSubmit={handleSubmit}
validateOn="submit"
>
<Field model="test.pass1">
<input />
</Field>
<Field model="test.pass2">
<input />
</Field>
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const [pass1, pass2] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'input');
it('should fail to submit with valid fields but an invalid form', () => {
TestUtils.Simulate.submit(formElement);
assert.isFalse(store.getState().testForm.$form.valid);
assert.isTrue(store.getState().testForm.$form.submitFailed);
pass1.value = 'aaa';
pass2.value = 'bbb';
TestUtils.Simulate.change(pass1);
TestUtils.Simulate.change(pass2);
TestUtils.Simulate.submit(formElement);
assert.isTrue(handleSubmit.callCount === 0);
assert.isTrue(store.getState().testForm.$form.submitFailed);
assert.isFalse(store.getState().testForm.$form.valid);
});
it('should submit with valid fields and a valid form', () => {
pass2.value = 'aaa';
TestUtils.Simulate.change(pass2);
TestUtils.Simulate.submit(formElement);
assert.isTrue(store.getState().testForm.$form.valid);
assert.isTrue(handleSubmit.calledOnce);
assert.isFalse(store.getState().testForm.$form.submitFailed);
assert.ok(store.getState().testForm.$form.valid);
});
});
describe('form revalidation after manual validation', () => {
const initialState = getInitialState({ foo: 'bar' });
const store = testCreateStore({
login: modelReducer('login', initialState),
loginForm: formReducer('login', initialState),
});
const handleSubmit = (values) => {
if (get(values, 'foo') !== 'changed') {
store.dispatch(actions.setValidity('login', {
correctDetails: false,
}));
}
};
const form = testRender(
<Form model="login" onSubmit={handleSubmit}>
<Field model="login.foo">
<input />
</Field>
</Form>, store);
const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
const formNode = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
it('should revalidate after being set invalid', () => {
TestUtils.Simulate.submit(formNode);
assert.isFalse(store.getState().loginForm.$form.valid);
input.value = 'changed';
TestUtils.Simulate.change(input);
TestUtils.Simulate.submit(formNode);
assert.isTrue(store.getState().loginForm.$form.valid);
});
});
describe('form reducer name isolation', () => {
const store = testCreateStore({
user: modelReducer('user'),
userForm: formReducer('user'),
userEx: modelReducer('userEx'),
userExForm: formReducer('userEx'),
});
const isRequired = (val) => val && val.length;
class UserForm extends React.Component {
componentDidMount() {
store.dispatch(actions.change('userEx', { username: '', email: '' }));
}
render() {
return (
<Form
model="userEx"
validators={{
username: isRequired,
email: isRequired,
}}
>
<Field model="userEx.username">
<input type="text" />
</Field>
<Field model="userEx.email">
<input type="text" />
</Field>
</Form>
);
}
}
TestUtils.renderIntoDocument(
<Provider store={store}>
<UserForm />
</Provider>
);
it('the similarly-named userEx form should not be valid in presence of'
+ 'valid user form', () => {
assert.isFalse(store.getState().userExForm.$form.valid);
});
});
describe('field validation and external changes', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
});
it('should validate form on external (async) change', () => {
const required = (val) => val && val.length;
TestUtils.renderIntoDocument(
<Provider store={store}>
<Form model="test">
<Field
model="test.foo"
validators={{ required }}
>
<input type="text" />
</Field>
<Field
model="test.bar"
validators={{ required }}
>
<input type="text" />
</Field>
</Form>
</Provider>
);
assert.isFalse(store.getState().testForm.$form.valid);
store.dispatch(actions.change('test', {
foo: 'foo valid',
bar: 'bar valid',
}));
assert.isTrue(store.getState().testForm.$form.valid);
});
});
xdescribe('reset event on form', () => {
it('should reset the model on the onReset event', () => {
const initialState = getInitialState({ foo: '' });
const store = testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
});
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form model="test">
<Field model="test.foo">
<input type="text" />
</Field>
<button type="reset" />
</Form>
</Provider>
);
const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
const reset = TestUtils.findRenderedDOMComponentWithTag(form, '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('programmatically submitting', () => {
it('the form node should be able to be submitted with submit()', () => {
const initialState = getInitialState({ foo: '' });
const store = testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
});
const handleSubmit = sinon.spy((val) => val);
const app = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
onSubmit={handleSubmit}
>
<Field
model="test.foo"
>
<input type="text" />
</Field>
</Form>
</Provider>
);
const form = TestUtils.findRenderedDOMComponentWithTag(app, 'form');
form.submit();
assert.isTrue(handleSubmit.calledOnce);
});
});
it('the form node should be able to be referenced', () => {
const initialState = getInitialState({ foo: '' });
const store = testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
});
const handleSubmit = sinon.spy((val) => val);
class App extends React.Component {
attachNode(node) {
this.node = findDOMNode(node);
}
handleClick() {
this.node.submit();
}
render() {
return (
<div>
<Form
model="test"
onSubmit={handleSubmit}
ref={this.attachNode.bind(this)}
>
<Field
model="test.foo"
>
<input type="text" />
</Field>
</Form>
<button onClick={this.handleClick.bind(this)} />
</div>
);
}
}
const app = TestUtils.renderIntoDocument(
<Provider store={store}>
<App />
</Provider>
);
const button = TestUtils.findRenderedDOMComponentWithTag(app, 'button');
TestUtils.Simulate.click(button);
assert.isTrue(handleSubmit.calledOnce);
});
describe('function as children', () => {
const initialState = getInitialState({ foo: 'bar' });
const store = testCreateStore({
test: modelReducer('test', initialState),
testForm: formReducer('test', initialState),
});
const form = testRender(
<Form model="test">
{(formValue) =>
<Field model={`${formValue.$form.model}.foo`}>
<input className={formValue.foo.focus ? 'focused' : ''} />
</Field>
}
</Form>, store);
const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
it('treats the return value as expected with normal children', () => {
assert.equal(input.value, 'bar');
input.value = 'testing';
TestUtils.Simulate.change(input);
assert.equal(input.value, 'testing');
assert.equal(get(store.getState().test, 'foo'), 'testing');
});
it('rerenders the function when the form value changes', () => {
assert.throws(() => TestUtils.findRenderedDOMComponentWithClass(form, 'focused'));
TestUtils.Simulate.focus(input);
assert.isTrue(store.getState().testForm.foo.focus);
assert.ok(TestUtils.findRenderedDOMComponentWithClass(form, 'focused'));
});
});
describe('validation on validation prop change', () => {
const initialState = getInitialState({
foo: '',
bar: '',
});
const store = testCreateStore({
testForm: formReducer('test'),
test: modelReducer('test', initialState),
});
let timesOneValidationCalled = 0;
let timesTwoValidationCalled = 0;
class ValidationChanger extends React.Component {
constructor() {
super();
this.state = { validateOne: true };
}
toggleValidateOne = () => {
this.setState({ validateOne: !this.state.validateOne });
};
validators() {
if (this.state.validateOne) {
return {
foo: () => {
timesOneValidationCalled += 1;
return true;
},
};
}
return {
foo: () => {
timesTwoValidationCalled += 1;
return true;
},
};
}
render() {
return (
<Provider store={store}>
<Form model="test" validators={this.validators()}>
<Field model="test.foo">
<input type="text" />
</Field>
<button onClick={this.toggleValidateOne}>Toggle One Validate</button>
</Form>
</Provider>
);
}
}
const form = TestUtils.renderIntoDocument(<ValidationChanger />);
const [toggleButton] = TestUtils.scryRenderedDOMComponentsWithTag(form, 'button');
it('should validate form validators initially on load', () => {
assert.equal(timesOneValidationCalled, 1);
assert.equal(timesTwoValidationCalled, 0);
});
it('should revalidate on form validators changing', () => {
TestUtils.Simulate.click(toggleButton);
assert.equal(timesOneValidationCalled, 1);
assert.equal(timesTwoValidationCalled, 1);
});
});
describe('validation of nested/deep model values', () => {
const initialState = getInitialState({
items: [
{ name: 'one' },
{ name: 'two' },
],
});
const store = testCreateStore({
testForm: formReducer('test', initialState),
test: modelReducer('test', initialState),
});
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="test"
validators={{
'items[].name': (name) => name && name.length,
}}
>
{get(initialState, 'items').map((item, i) =>
<Control model={`test.items[${i}].name`} key={`test.items[${i}].name`} />
)}
</Form>
</Provider>
);
const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const [_, input2] = TestUtils
.scryRenderedDOMComponentsWithTag(form, 'input');
it('should initially validate each item', () => {
const { $form, items } = store.getState().testForm;
assert.isTrue(items[0].name.valid);
assert.isTrue(items[1].name.valid);
assert.isTrue($form.valid);
});
it('after submit should stay valid', () => {
TestUtils.Simulate.submit(formElement);
const { $form, items } = store.getState().testForm;
assert.isTrue(items[0].name.valid);
assert.isTrue(items[1].name.valid);
assert.isTrue($form.valid);
});
it('should check validity of each item on change', () => {
input2.value = '';
TestUtils.Simulate.change(input2);
const { $form, items } = store.getState().testForm;
assert.isTrue(items[0].name.valid);
assert.isFalse(items[1].name.valid);
assert.isFalse($form.valid);
});
});
describe('submit after field invalid', () => {
const initialState = getInitialState({ username: '' });
const store = testCreateStore({
login: modelReducer('login', initialState),
loginForm: formReducer('login', initialState),
});
const handleSubmit = sinon.spy((val) => val);
const form = TestUtils.renderIntoDocument(
<Provider store={store}>
<Form
model="login"
onSubmit={handleSubmit}
validators={{ username: (val) => !!val }}
validateOn="submit"
>
<Field model="login.username">
<input />
</Field>
</Form>
</Provider>
);
const formNode = TestUtils.findRenderedDOMComponentWithTag(form, 'form');
const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input');
it('should be invalid after initial submit', () => {
TestUtils.Simulate.submit(formNode);
assert.isTrue(handleSubmit.callCount === 0);
assert.isFalse(store.getState().loginForm.$form.valid);
assert.isFalse(store.getState().loginForm.username.valid);
});
it('should submit after found to be valid', () => {
input.value = 'changed';
TestUtils.Simulate.change(input);
assert.isFalse(store.getState().loginForm.username.valid,
'should not be valid yet');
TestUtils.Simulate.submit(formNode);
assert.isTrue(store.getState().loginForm.$form.valid);
assert.isTrue(store.getState().loginForm.username.valid);
assert.isTrue(handleSubmit.calledOnce);
});
});
describe('form-wide validation with no form validators', () => {
const initialState = getInitialState({ foo: ''