@kabbi/react-redux-form
Version:
Create Forms Easily with React and Redux
766 lines (720 loc) • 20.1 kB
JavaScript
import { assert } from 'chai';
import {
actions,
actionTypes,
initialFieldState,
form as selectForm,
formReducer,
combineForms,
} from '../src';
import { createStore } from 'redux';
import mapValues from '../src/utils/map-values';
import toPath from 'lodash.topath';
import get from '../src/utils/get';
describe('formReducer() (V1)', () => {
it('should exist as a function', () => {
assert.isFunction(formReducer);
});
const formActionsSpec = {
[actionTypes.CHANGE]: [
{
action: actions.change,
args: ['foo'],
expectedField: {
pristine: false,
validated: false,
value: 'foo',
},
expectedForm: {
pristine: false,
},
},
{
action: actions.change,
args: [{ foo: 'bar' }],
expectedField: {
$form: {
pristine: false,
validated: false,
value: { foo: 'bar' },
},
},
expectedForm: {
pristine: false,
},
},
{
action: actions.change,
args: [[1, 2, 3]],
expectedField: {
$form: {
pristine: false,
validated: false,
value: [1, 2, 3],
},
},
expectedForm: {
pristine: false,
},
},
{
action: actions.load,
args: ['string'],
expectedField: {
pristine: true,
value: 'string',
loadedValue: 'string',
},
expectedForm: {
pristine: true,
},
},
{
action: actions.load,
args: [42],
expectedField: {
pristine: true,
value: 42,
loadedValue: 42,
},
expectedForm: {
pristine: true,
},
},
{
action: actions.load,
args: [{ foo: 'bar' }],
expectedField: {
$form: {
pristine: true,
value: { foo: 'bar' },
loadedValue: { foo: 'bar' },
},
foo: {
pristine: true,
value: 'bar',
initialValue: 'bar',
},
},
expectedForm: {
pristine: true,
},
},
],
[actionTypes.FOCUS]: [
{
action: actions.focus,
args: [],
expectedField: { focus: true },
},
],
[actionTypes.SET_PRISTINE]: [
{
action: actions.setPristine,
args: [],
expectedField: { pristine: true },
},
{
action: actions.setPristine,
initialState: {
$form: {
...initialFieldState,
pristine: false,
},
name: {
...initialFieldState,
pristine: false,
},
},
args: [],
expectedField: { pristine: true },
expectedForm: { pristine: true },
},
{
action: actions.setPristine,
initialState: {
$form: {
...initialFieldState,
pristine: false,
},
name: {
...initialFieldState,
pristine: false,
},
other: {
...initialFieldState,
pristine: true,
},
},
args: [],
expectedField: { pristine: true },
expectedForm: { pristine: true },
},
{
action: actions.setPristine,
initialState: {
$form: {
...initialFieldState,
pristine: false,
},
name: {
...initialFieldState,
pristine: false,
},
other: {
...initialFieldState,
pristine: false,
},
},
args: [],
expectedField: { pristine: true },
expectedForm: { pristine: false },
},
],
[actionTypes.SET_DIRTY]: [
{
action: actions.setDirty,
args: [],
expectedField: { pristine: false },
},
],
[actionTypes.BLUR]: [
{
action: actions.blur,
args: [],
expectedField: {
focus: false,
touched: true,
},
expectedForm: {
touched: true,
},
},
{
label: 'after submitted',
action: actions.blur,
initialState: {
$form: {
...initialFieldState,
submitted: true,
retouched: false,
},
name: {
...initialFieldState,
retouched: false,
},
},
expectedField: {
focus: false,
touched: true,
retouched: true,
},
expectedForm: {
touched: true,
retouched: true,
},
},
],
[actionTypes.SET_UNTOUCHED]: [
{
action: actions.setUntouched,
args: [],
expectedField: { touched: false },
},
],
[actionTypes.SET_TOUCHED]: [
{
action: actions.setTouched,
args: [],
expectedField: { touched: true },
expectedForm: { touched: true },
},
],
[actionTypes.SET_PENDING]: [
{
action: actions.setPending,
args: [],
expectedForm: {
pending: true,
retouched: false,
},
expectedField: {
pending: true,
submitted: false,
submitFailed: false,
retouched: false,
},
},
{
action: actions.setPending,
args: [],
initialState: {
$form: {
...initialFieldState,
retouched: true,
},
},
expectedForm: {
pending: true,
},
expectedField: {
pending: true,
submitted: false,
submitFailed: false,
retouched: false,
},
},
],
[actionTypes.SET_VALIDITY]: [
{
action: actions.setValidity,
args: [{ foo: true }],
expectedField: {
validity: { foo: true },
errors: { foo: false },
validated: true,
},
},
{
action: actions.setValidity,
args: [{ foo: false }],
expectedField: {
validity: { foo: false },
errors: { foo: true },
validated: true,
},
},
{
action: actions.setValidity,
args: [{ foo: false, bar: true }],
expectedField: {
validity: { foo: false, bar: true },
errors: { foo: true, bar: false },
validated: true,
},
},
{
label: 'validating the form (invalid)',
action: actions.setValidity,
model: 'user',
args: [{ foo: false, bar: true }],
expectedForm: {
validity: { foo: false, bar: true },
errors: { foo: true, bar: false },
validated: true,
},
},
{
label: 'validating the form (valid)',
action: actions.setValidity,
model: 'user',
args: [{ foo: true, bar: true }],
expectedForm: {
validity: { foo: true, bar: true },
errors: { foo: false, bar: false },
validated: true,
},
},
],
[actionTypes.RESET_VALIDITY]: [
{
action: actions.resetValidity,
model: 'user',
initialState: {
$form: {
...initialFieldState,
valid: false,
validity: { foo: false },
errors: { foo: true },
},
name: {
...initialFieldState,
valid: false,
validity: { required: false },
errors: { required: true },
},
},
expectedField: {
validity: {},
errors: {},
valid: true,
},
expectedForm: {
validity: {},
errors: {},
valid: true,
},
expectedSubField: {
validity: {},
errors: {},
valid: true,
},
},
],
[actionTypes.SET_ERRORS]: [
{
label: '1',
action: actions.setErrors,
args: [{ foo: true }],
expectedField: {
validity: { foo: false },
errors: { foo: true },
validated: true,
},
},
{
label: '2',
action: actions.setErrors,
args: [{ foo: false }],
expectedField: {
validity: { foo: true },
errors: { foo: false },
validated: true,
},
},
{
label: '3',
action: actions.setErrors,
args: [{ foo: false, bar: true }],
expectedField: {
validity: { foo: true, bar: false },
errors: { foo: false, bar: true },
validated: true,
},
},
{
label: 'single string error message',
action: actions.setErrors,
args: ['single error message'],
expectedField: {
errors: 'single error message',
},
},
{
label: 'validating the form (invalid)',
action: actions.setErrors,
model: 'user',
args: [{ foo: false, bar: true }],
expectedForm: {
validity: { foo: true, bar: false },
errors: { foo: false, bar: true },
validated: true,
},
},
{
label: 'validating the form (invalid)',
action: actions.setErrors,
model: 'user',
args: [{ foo: true, bar: true }],
expectedForm: {
validity: { foo: false, bar: false },
errors: { foo: true, bar: true },
validated: true,
},
},
{
label: 'validating the form (valid)',
action: actions.setErrors,
model: 'user',
args: [{ foo: false, bar: false }],
expectedForm: {
validity: { foo: true, bar: true },
errors: { foo: false, bar: false },
validated: true,
},
},
],
[actionTypes.SET_FIELDS_VALIDITY]: [
{
action: actions.setFieldsValidity,
model: 'user',
args: [{ foo: false, bar: false }],
expectedForm: (form) =>
form.foo.errors === true
&& form.bar.errors === true,
},
{
action: actions.setFieldsValidity,
model: 'user.deep',
args: [{ foo: false, bar: false }],
expectedForm: (form) =>
form.deep.foo.errors === true
&& form.deep.bar.errors === true,
},
{
action: actions.setFieldsValidity,
model: 'user',
args: [{ foo: true, bar: true }, { errors: true }],
expectedForm: (form) =>
form.foo.errors === true
&& form.bar.errors === true,
},
{
action: actions.setFieldsValidity,
model: 'user.deep',
args: [{ foo: true, bar: true }, { errors: true }],
expectedForm: (form) =>
form.deep.foo.errors === true
&& form.deep.bar.errors === true,
},
{
label: 'form-wide boolean validity',
action: actions.setFieldsValidity,
model: 'user',
args: [{ '': false }],
expectedForm: (form) =>
form.$form.errors === true
&& form.$form.valid === false,
},
{
label: 'form-wide object errors validity',
action: actions.setFieldsValidity,
model: 'user',
initialState: {
$form: initialFieldState,
name: {
...initialFieldState,
valid: false,
validity: false,
errors: true,
},
},
args: [{ '': { passMatch: true } }],
expectedForm: (form) =>
form.$form.validity.passMatch === true
&& form.$form.valid === false,
},
],
[actionTypes.SET_SUBMITTED]: [
{
action: actions.setSubmitted,
args: [],
expectedForm: (form) => selectForm(form).touched,
expectedField: {
pending: false,
submitted: true,
touched: true,
retouched: false,
},
},
],
[actionTypes.SET_SUBMIT_FAILED]: [
{
action: actions.setSubmitFailed,
model: 'user',
args: [],
initialState: {
$form: initialFieldState,
name: initialFieldState,
deep: {
$form: initialFieldState,
foo: initialFieldState,
bar: initialFieldState,
},
},
expectedForm: (form) => selectForm(form).touched,
expectedField: {
pending: false,
submitted: false,
submitFailed: true,
touched: true,
retouched: false,
},
expectedSubField: {
pending: false,
submitted: false,
submitFailed: true,
touched: true,
retouched: false,
},
},
],
};
mapValues(formActionsSpec, (tests, actionType) => tests.forEach(({
action,
args = [],
expectedForm,
expectedField,
expectedSubField,
initialState = undefined,
label = '',
model = 'user.name',
}) => {
describe(`${actionType} action ${label}`, () => {
const modelPath = toPath(model);
const localModelPath = modelPath.slice(1);
const localFormPath = localModelPath.slice(0, -1);
const reducer = formReducer('user', { name: '' });
const updatedState = reducer(initialState, action(model, ...args));
if (expectedField) {
it('should properly set the field state', () => {
const updatedFieldState = localModelPath.length
? get(updatedState, localModelPath)
: updatedState.$form;
assert.containSubset(
updatedFieldState,
expectedField);
});
}
if (expectedSubField) {
it('should properly set the state of the child fields', () => {
const localFieldsPath = localModelPath.slice(0, -1);
const updatedFieldsState = localFieldsPath.length
? get(updatedState, localFieldsPath)
: updatedState;
function checkSubFields(subFields) {
mapValues(subFields, (subField, key) => {
if (key === '$form') return;
if (subField.$form) {
checkSubFields(subField);
} else {
assert.containSubset(
subField,
expectedSubField);
}
});
}
checkSubFields(updatedFieldsState);
});
}
if (expectedForm) {
const form = get(updatedState, localFormPath);
it('should properly set the form state', () => {
if (typeof expectedForm === 'function') {
assert.ok(expectedForm(form));
} else {
assert.containSubset(
form.$form,
expectedForm);
}
});
}
});
}));
describe('valid state of parent forms', () => {
const reducer = formReducer('test', {
foo: 'foo',
meta: {
bar: 'deep',
},
});
const invalidFoo = reducer(undefined,
actions.setValidity('test.foo', false));
it('parent form should be invalid if child is invalid', () => {
assert.isFalse(invalidFoo.$form.valid);
assert.isFalse(invalidFoo.foo.valid);
});
const invalidFooBar = reducer(invalidFoo,
actions.setValidity('test.meta.bar', false));
it('parent form should remain invalid if grandchild is invalid', () => {
assert.isFalse(invalidFooBar.$form.valid);
assert.isFalse(invalidFooBar.foo.valid);
assert.isFalse(invalidFooBar.meta.bar.valid);
});
const focusedNewField = reducer(invalidFooBar,
actions.focus('test.meta.new', true));
it('parent form should remain invalid if new field dynamically added', () => {
assert.isFalse(focusedNewField.$form.valid);
assert.isFalse(focusedNewField.foo.valid);
assert.isFalse(focusedNewField.meta.bar.valid);
assert.isTrue(focusedNewField.meta.new.valid);
});
const invalidFooValidBar = reducer(focusedNewField,
actions.setValidity('test.meta.bar', true));
it('parent form should remain invalid if only grandchild is valid', () => {
assert.isFalse(invalidFooValidBar.$form.valid);
assert.isFalse(invalidFooValidBar.foo.valid);
assert.isTrue(invalidFooValidBar.meta.$form.valid);
assert.isTrue(invalidFooValidBar.meta.bar.valid);
});
const validFooValidBar = reducer(invalidFooValidBar,
actions.setValidity('test.foo', true));
it('parent form should be valid if all descendants are valid', () => {
assert.isTrue(validFooValidBar.foo.valid);
assert.isTrue(validFooValidBar.meta.$form.valid);
assert.isTrue(validFooValidBar.meta.bar.valid);
assert.isTrue(validFooValidBar.$form.valid);
});
});
describe('deep resetting', () => {
it('resetting a parent field should reset child fields in form', () => {
const reducer = formReducer('test', {
foo: '',
meta: {
bar: '',
},
});
const changedState = reducer(undefined, actions.change('test', {
foo: 'changed foo',
meta: { bar: 'changed bar' },
}));
const resetState = reducer(changedState, actions.reset('test'));
assert.equal(resetState.foo.value, '');
assert.equal(resetState.meta.bar.value, '');
});
it('resetting a parent field should reset complex child fields', () => {
const reducer = formReducer('test', {
foo: [],
meta: {
bar: [],
},
});
const changedState = reducer(undefined, actions.change('test', {
foo: [1, 2, 3],
meta: { bar: [1, 2, 3] },
}));
const resetState = reducer(changedState, actions.reset('test'));
assert.deepEqual(resetState.foo.$form.value, []);
assert.deepEqual(resetState.meta.bar.$form.value, []);
});
});
describe('resetting after load', () => {
const reducer = formReducer('test', {
foo: '',
});
const loadedState = reducer(undefined,
actions.load('test.foo', 'new loaded'));
it('should change the loaded value for the field', () => {
assert.equal(loadedState.foo.loadedValue, 'new loaded');
assert.equal(loadedState.foo.value, 'new loaded');
});
it('should change the loaded value for the form', () => {
assert.deepEqual(loadedState.$form.loadedValue, { foo: 'new loaded' });
assert.deepEqual(loadedState.$form.value, { foo: 'new loaded' });
});
it('resetting a parent field should reset child fields in form', () => {
const resetState = reducer(loadedState, actions.reset('test'));
assert.deepEqual(resetState.$form.value, { foo: 'new loaded' });
assert.equal(resetState.foo.value, 'new loaded');
});
});
describe('resetting with null', () => {
it('should work and not cause an infinite loop', () => {
assert.doesNotThrow(() => {
const reducer = formReducer('foo', null);
const state = reducer(
undefined,
actions.reset('foo')
);
assert.containSubset(state, {
value: null,
});
});
});
it('should reset between null and complex values', () => {
const store = createStore(combineForms({
user: {
city: null,
},
}));
// User selected new city
store.dispatch(actions.change('user.city', { id: 42, title: 'London' }));
// User resetted the form
store.dispatch(actions.reset('user'));
// What if we want to reset the field externally? It's already null, nothing bad could happen
store.dispatch(actions.change('user.city', null));
// This should not throw anymore
assert.doesNotThrow(() => store.dispatch(actions.resetValidity('user')));
});
});
});