UNPKG

@ngbrown/react-redux-form

Version:

Create Forms Easily with React and Redux

1,806 lines (1,459 loc) 51.5 kB
import { assert } from 'chai'; import configureMockStore from 'redux-mock-store'; import createTestStore from 'redux-test-store'; import thunk from 'redux-thunk'; import _get from 'lodash.get'; import toPath from 'lodash.topath'; import i from 'icepick'; import Immutable from 'immutable'; import { testCreateStore } from './utils'; import isValid from '../src/form/is-valid'; import isPristine from '../src/form/is-pristine'; import isRetouched from '../src/form/is-retouched'; import { actions as _actions, actionTypes, formReducer as _formReducer, initialFieldState, } from '../src'; import { actions as immutableActions, formReducer as immutableFormReducer, } from '../immutable'; const testContexts = { standard: { actions: _actions, formReducer: _formReducer, object: {}, get: _get, set: (state, path, value) => i.setIn(state, path, value), getInitialState: (state) => state, }, immutable: { actions: immutableActions, formReducer: immutableFormReducer, 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), }, }; Object.keys(testContexts).forEach((testKey) => { const testContext = testContexts[testKey]; const actions = testContext.actions; const formReducer = testContext.formReducer; const getInitialState = testContext.getInitialState; describe('field actions', () => { describe('change()', () => { it('should set the retouched property to true upon change after submit', () => { const initialState = getInitialState({ foo: '' }); const reducer = formReducer('test', initialState); const state = reducer(undefined, actions.setSubmitted('test')); assert.containSubset( state.$form, { submitted: true, retouched: false, }, 'not retouched yet'); const changedState = reducer(state, actions.change('test.foo', 'new')); assert.containSubset( changedState.$form, { submitted: true, }); assert.isTrue(isRetouched(changedState), 'form retouched after submit'); assert.containSubset( changedState.foo, { retouched: true, }, 'field retouched after submit'); }); it('should be able to set change an object to null without throwing an error', () => { // this is a weird one, but because $form is now // stuffed into the store in each model that is an object // the sub fields updater expects to find it. // so if it is studdenly changed to null, // the .$form breaks. in the code, we just added // a condition like subField && subField.$form. // this test will ensure that condition stays functioning const reducer = formReducer('foo', { a: {} }); let didThrow = false; try { reducer( undefined, actions.change('foo.a', null) ); } catch (e) { didThrow = true; } assert.isFalse(didThrow); }); }); describe('reset()', () => { it('should set the field to the initial field state', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.reset('test.foo')) .foo, initialFieldState); }); it('should be able to set the entire form to the initial state', () => { const initialState = getInitialState({ foo: 'bar' }); const reducer = formReducer('test', initialState); const localInitialFormState = reducer(undefined, 'BOGUS / INITIAL STATE'); assert.containSubset( reducer(undefined, actions.reset('test')), localInitialFormState); }); it('should reset all errors on the field', () => { const reducer = formReducer('test'); const stateWithErrors = reducer(undefined, actions.setValidity('test.foo', { valid: false, required: true, })); assert.deepEqual(stateWithErrors.foo.errors, { valid: true, required: false, }); const stateAfterReset = reducer(stateWithErrors, actions.reset('test.foo')); assert.deepEqual(stateAfterReset.foo.errors, {}); }); it('should reset all errors on the form', () => { const reducer = formReducer('test'); const stateWithErrors = reducer(undefined, actions.setValidity('test', { valid: false, required: true, })); assert.deepEqual(stateWithErrors.$form.errors, { valid: true, required: false, }); assert.deepEqual(stateWithErrors.$form.validity, { valid: false, required: true, }); const stateAfterReset = reducer(stateWithErrors, actions.reset('test')); assert.deepEqual(stateAfterReset.$form.errors, {}); }); it('should intend to revalidate the field and subfields', () => { const reducer = formReducer('test', { button: {}, }); const resetState = reducer(undefined, actions.reset('test')); assert.include(resetState.$form.intents, { type: 'reset' }); assert.include(resetState.button.$form.intents, { type: 'reset' }, 'should intend to revalidate subfields (handled with reset)'); }); }); describe('focus()', () => { it('should set the focus state of the field to true', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.focus('test.foo')) .foo, { focus: true, }); }); }); describe('blur()', () => { it('should set the focus state to false', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.blur('test.foo')) .foo, { focus: false, touched: true, }); }); }); describe('setPristine()', () => { it('should set the pristine state of the field to true', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setPristine('test.foo')) .foo, { pristine: true, }); }); it('should set the pristine state of the form to true if every ' + 'field is pristine', () => { const reducer = formReducer('test'); const actualPristine = reducer(undefined, actions.setPristine('test.foo')); assert.containSubset( actualPristine.$form, { pristine: true, }); const actualDirty = reducer(actualPristine, actions.setDirty('test.bar')); assert.isFalse(isPristine(actualDirty)); const actualMultiplePristine = reducer(actualDirty, actions.setPristine('test.bar')); assert.isTrue(isPristine(actualMultiplePristine)); }); it('should be able to set the pristine state of the form and each field to true', () => { const reducer = formReducer('test'); const dirtyFormAndField = reducer(undefined, actions.setDirty('test.foo')); const pristineFormAndField = reducer(dirtyFormAndField, actions.setPristine('test')); assert.isTrue(isPristine(pristineFormAndField)); assert.containSubset( pristineFormAndField.foo, { pristine: true, }); }); }); describe('setDirty()', () => { it('should set pristine state to false', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setDirty('test.foo')) .foo, { pristine: false, }); }); it('should set pristine form state to false', () => { const reducer = formReducer('test'); const actual = reducer(undefined, actions.setDirty('test.foo')); assert.isFalse(isPristine(actual)); }); }); describe('setPending()', () => { it('should set the pending state of the field to true ' + 'and the submitted state to false', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setPending('test.foo')) .foo, { pending: true, submitted: false, }); }); it('should set the pending state of the field to the specified state', () => { const reducer = formReducer('test'); const actualPending = reducer(undefined, actions.setPending('test.foo', true)); assert.containSubset( actualPending .foo, { pending: true, submitted: false, }); const actualNotPending = reducer(actualPending, actions.setPending('test.foo', false)); assert.containSubset( actualNotPending .foo, { pending: false, submitted: false, }); }); it('should work with forms', () => { const reducer = formReducer('test'); const actualPending = reducer(undefined, actions.setPending('test')); assert.containSubset( actualPending.$form, { pending: true, submitted: false, }); assert.containSubset( reducer(actualPending, actions.setPending('test', false)) .$form, { pending: false, submitted: false, }); }); }); describe('setValidating()', () => { it('should set the validating state of the field to true', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setValidating('test.foo')) .foo, { validating: true, validated: false, }); }); it('should set the validating state of the field to the specified state', () => { const reducer = formReducer('test'); const actualValidating = reducer( undefined, actions.setValidating('test.foo', true)); assert.containSubset( actualValidating .foo, { validating: true, validated: false, }); const actualNotValidating = reducer( actualValidating, actions.setValidating('test.foo', false)); assert.containSubset( actualNotValidating .foo, { validating: false, validated: true, }); }); it('should work with forms', () => { const reducer = formReducer('test'); const actualValidating = reducer(undefined, actions.setValidating('test')); assert.containSubset( actualValidating.$form, { validating: true, validated: false, }); assert.containSubset( reducer(actualValidating, actions.setValidating('test', false)) .$form, { validating: false, validated: true, }); }); }); describe('setSubmitted()', () => { it('should set the submitted state of the field to true ' + 'and the pending state to false', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setSubmitted('test.foo')) .foo, { submitted: true, pending: false, touched: true, }); }); it('should set the submitted state of the field to the specified state', () => { const reducer = formReducer('test'); const actualSubmitted = reducer(undefined, actions.setSubmitted('test.foo', true)); assert.containSubset( actualSubmitted .foo, { submitted: true, pending: false, touched: true, }); const actualNotSubmitted = reducer( actualSubmitted, actions.setSubmitted('test.foo', false)); assert.containSubset( actualNotSubmitted .foo, { submitted: false, pending: false, touched: true, }); }); it('should work with forms', () => { const reducer = formReducer('test'); const actualSubmitted = reducer(undefined, actions.setSubmitted('test', true)); assert.containSubset( actualSubmitted.$form, { submitted: true, pending: false, }); const actualNotSubmitted = reducer(actualSubmitted, actions.setSubmitted('test', false)); assert.containSubset( actualNotSubmitted.$form, { submitted: false, pending: false, }); }); }); describe('setSubmitFailed()', () => { it('should set the submitFailed property to true', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setSubmitFailed('test')) .$form, { submitFailed: true, submitted: false, pending: false, }); }); it('should set the submitFailed property to false after successful submit', () => { const reducer = formReducer('test'); const state = reducer(undefined, actions.setSubmitFailed('test')); assert.containSubset( reducer(state, actions.setSubmitted('test')) .$form, { submitFailed: false, submitted: true, pending: false, }); }); it('should set the submitFailed property to false while pending', () => { const reducer = formReducer('test'); const state = reducer(undefined, actions.setSubmitFailed('test')); assert.containSubset( reducer(state, actions.setPending('test')) .$form, { submitFailed: false, submitted: false, pending: true, }); }); it('should set pending and submitted to false', () => { const reducer = formReducer('test'); let state = reducer(undefined, actions.setPending('test')); assert.containSubset( reducer(state, actions.setSubmitFailed('test')) .$form, { submitFailed: true, submitted: false, pending: false, }, 'should set pending to false'); state = reducer(state, actions.setSubmitted('test')); assert.containSubset( reducer(state, actions.setSubmitFailed('test')) .$form, { submitFailed: true, submitted: false, pending: false, }, 'should set submitted to false'); }); }); describe('setTouched()', () => { it('should set the touched and blurred state of the field to true ' + 'and the untouched and focused state to false', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setTouched('test.foo')) .foo, { touched: true, retouched: false, focus: false, }); }); it('should set the retouched property to true upon touch after submit', () => { const reducer = formReducer('test'); const state = reducer(undefined, actions.setSubmitted('test')); assert.containSubset( state.$form, { submitted: true, retouched: false, }, 'not retouched yet'); assert.containSubset( reducer(state, actions.setTouched('test')) .$form, { submitted: true, retouched: true, }, 'retouched after submit'); }); it('should set the retouched property to true upon touch after submit failed', () => { const reducer = formReducer('test'); const state = reducer(undefined, actions.setSubmitFailed('test')); assert.containSubset( state.$form, { submitted: false, submitFailed: true, retouched: false, }, 'not retouched yet'); assert.containSubset( reducer(state, actions.setTouched('test')) .$form, { submitted: false, submitFailed: true, retouched: true, }, 'retouched after submit failed'); }); it('should set the retouched property to false when pending', () => { const reducer = formReducer('test'); let state = reducer(undefined, actions.setSubmitted('test')); state = reducer(state, actions.setTouched('test')); assert.containSubset( state.$form, { submitted: true, retouched: true, }, 'retouched after submit and before pending'); state = reducer(state, actions.setPending('test')); assert.containSubset( state.$form, { pending: true, submitted: false, retouched: false, }, 'not retouched while pending'); }); }); describe('setUntouched()', () => { it('should set the touched state to false', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setUntouched('test.foo')) .foo, { touched: false, }); }); }); describe('setValidity()', () => { it('should set the errors state of the field the inverse of a boolean validity', () => { const reducer = formReducer('test'); assert.containSubset( reducer(undefined, actions.setValidity('test.foo', true)) .foo, { errors: false, }); assert.containSubset( reducer(undefined, actions.setValidity('test.foo', false)) .foo, { errors: true, }); }); it('should set the errors state of the field the inverse of each value of a validity object', () => { const reducer = formReducer('test'); const validity = { good: true, bad: false, }; assert.containSubset( reducer(undefined, actions.setValidity('test.foo', validity)) .foo, { errors: { good: false, bad: true, }, }); }); it('should set the valid state to true if all values in validity object are true', () => { const reducer = formReducer('test'); const validity = { one: true, two: true, }; const actualForm = reducer(undefined, actions.setValidity('test.foo', validity)); assert.isTrue(isValid(actualForm.foo)); assert.isTrue(isValid(actualForm), 'form should be valid if all fields are valid'); }); it('should set the valid state to false if any value in validity object are false', () => { const reducer = formReducer('test'); const validity = { one: true, two: true, three: false, }; const actualForm = reducer(undefined, actions.setValidity('test.foo', validity)); assert.isFalse(isValid(actualForm.foo)); assert.isFalse(isValid(actualForm), 'form should be invalid if any fields are invalid'); }); it('should be able to set the validity of a form', () => { const reducer = formReducer('test'); const validity = { foo: true, baz: false, }; const actual = reducer( undefined, actions.setValidity('test', validity)); assert.containSubset( actual.$form, { errors: { foo: false, baz: true, }, }); assert.isFalse(isValid(actual)); }); it('should be able to set the validity to a non-boolean value', () => { const reducer = formReducer('test'); const validity = { foo: 'truthy string', baz: null, // false value }; let actual = reducer( undefined, actions.setValidity('test', validity)); assert.containSubset( actual.$form, { validity: { foo: 'truthy string', baz: null, }, errors: { foo: false, baz: true, }, }); assert.isFalse(isValid(actual)); actual = reducer(actual, actions.setValidity('test', { foo: false, })); assert.containSubset( actual.$form, { validity: { foo: false, }, errors: { foo: true, }, }); assert.isFalse(isValid(actual)); actual = reducer(actual, actions.setValidity('test', { foo: 'truthy string', baz: 100, })); assert.containSubset( actual.$form, { validity: { foo: 'truthy string', baz: 100, }, errors: { foo: false, baz: false, }, }); assert.isTrue(isValid(actual)); }); it('should overwrite the previous validity', () => { const reducer = formReducer('test'); const oldValidity = { existing: true, }; const oldState = reducer( undefined, actions.setValidity('test', oldValidity)); assert.deepEqual( oldState.$form.validity, oldValidity); const newValidity = { foo: true, bar: false, }; const newState = reducer( oldState, actions.setValidity('test', newValidity)); assert.deepEqual( newState.$form.validity, newValidity); assert.deepEqual( newState.$form.errors, { foo: false, bar: true, }); }); }); describe('setErrors()', () => { it('should set the errors state of the field', () => { const reducer = formReducer('test'); const actualInvalid = reducer(undefined, actions.setErrors('test.foo', true)); assert.containSubset( actualInvalid.foo, { errors: true, validity: false, }); assert.isFalse(isValid(actualInvalid.foo)); const actualValid = reducer(undefined, actions.setErrors('test.foo', false)); assert.containSubset( actualValid.foo, { errors: false, validity: true, }); assert.isTrue(isValid(actualValid.foo)); }); it('should set the errors state of the field', () => { const reducer = formReducer('test'); const errors = { good: true, bad: false, }; const actual = reducer(undefined, actions.setErrors('test.foo', errors)); assert.containSubset( actual.foo, { errors: { good: true, bad: false, }, validity: { good: false, bad: true, }, }); assert.isFalse(isValid(actual.foo)); }); it('should set the valid state to true if all values in error object are false', () => { const reducer = formReducer('test'); const errors = { one: false, two: null, three: 0, }; const actualForm = reducer(undefined, actions.setErrors('test.foo', errors)); assert.containSubset( actualForm.foo, { validity: { one: true, two: true, three: true, }, }); assert.isTrue(isValid(actualForm.foo)); assert.isTrue(isValid(actualForm), 'form should be valid if all fields are valid'); }); it('should set the valid state to false if any value in error object is true', () => { const reducer = formReducer('test'); const errors = { one: true, two: false, three: false, }; const actualForm = reducer(undefined, actions.setErrors('test.foo', errors)); assert.containSubset( actualForm.foo, { errors: { one: true, two: false, three: false, }, validity: { one: false, two: true, three: true, }, }); assert.isFalse(isValid(actualForm.foo)); assert.isFalse(isValid(actualForm), 'form should be invalid if any fields are invalid'); }); it('should be able to set the errors of a form', () => { const reducer = formReducer('test'); const errors = { foo: true, baz: false, }; const actual = reducer( undefined, actions.setErrors('test', errors)); assert.containSubset( actual.$form, { errors: { foo: true, baz: false, }, }); assert.isFalse(isValid(actual)); }); it('should be able to set the errors to an object', () => { const reducer = formReducer('test'); const errors = { foo: 'foo is required', baz: null, // falsey value }; let actual = reducer( undefined, actions.setErrors('test', errors)); assert.containSubset( actual.$form, { validity: { foo: false, baz: true, }, errors: { foo: 'foo is required', baz: null, }, }); assert.isFalse(isValid(actual)); actual = reducer(actual, actions.setErrors('test', { foo: false, })); assert.containSubset( actual.$form, { validity: { foo: true, }, errors: { foo: false, }, }); assert.isTrue(isValid(actual)); actual = reducer(actual, actions.setErrors('test', { foo: 'foo is required', baz: 'baz is also required', })); assert.containSubset( actual.$form, { validity: { foo: false, baz: false, }, errors: { foo: 'foo is required', baz: 'baz is also required', }, }); assert.isFalse(isValid(actual)); }); it('should be able to set the errors to a string', () => { const reducer = formReducer('test'); const errors = 'This whole thing is invalid'; const fieldErrors = 'This field is invalid'; let actual = reducer( undefined, actions.setErrors('test', errors)); assert.containSubset( actual.$form, { errors, validity: false, }); assert.isFalse(isValid(actual)); actual = reducer( actual, actions.setErrors('test', false)); assert.containSubset( actual.$form, { errors: false, validity: true, }); assert.isTrue(isValid(actual)); let actualField = reducer( undefined, actions.setErrors('test.foo', fieldErrors)); assert.containSubset( actualField.foo, { errors: fieldErrors, validity: false, }); assert.isFalse(isValid(actualField)); actualField = reducer( actualField, actions.setErrors('test.foo', false)); assert.containSubset( actualField.foo, { errors: false, validity: true, }); assert.isTrue(isValid(actualField)); }); }); describe('setInitial()', () => { it('should set to initial field state without changing model value', () => { const reducer = formReducer('test', { foo: 'bar' }); const changedState = reducer(undefined, actions.change('test.foo', 'changed')); assert.containSubset( reducer(changedState, actions.setInitial('test.foo')) .foo, { ...initialFieldState, value: 'changed', }); }); it('should maintain the expected state shape in sub forms', () => { const reducer = formReducer('test', { somePrimitive: 1, someArray: [] }); const changedState = reducer(undefined, actions.setInitial('test')); assert.isUndefined(changedState.someArray.$form.$form); }); }); describe('resetValidity() and resetErrors()', () => { const reducer = formReducer('test'); it('should reset the validity and errors of a field state', () => { const stateWithErrors = reducer( undefined, actions.setErrors('test.foo', { bar: true, baz: true })); assert.containSubset( stateWithErrors.foo, { validity: { bar: false, baz: false }, errors: { bar: true, baz: true }, }); assert.isFalse(isValid(stateWithErrors.foo)); const actualState = reducer( stateWithErrors, actions.resetValidity('test.foo')); assert.containSubset( actualState.foo, { validity: {}, errors: {}, }); assert.isTrue(isValid(actualState.foo)); }); it('should handle omitKeys option beeing empty array', () => { const stateWithErrors = reducer( undefined, actions.setErrors('test.foo', 'Foo')); assert.containSubset( stateWithErrors.foo, { validity: false, errors: 'Foo', }); assert.isFalse(isValid(stateWithErrors.foo)); const actualState = reducer( stateWithErrors, actions.resetValidity('test.foo', [])); assert.containSubset( actualState.foo, { validity: {}, errors: {}, }); assert.isTrue(isValid(actualState.foo)); }); it('should reset errors of a field with string errors', () => { const stateWithErrors = reducer( undefined, actions.setErrors('test.foo', 'Foo')); assert.containSubset( stateWithErrors.foo, { validity: false, errors: 'Foo', }); assert.isFalse(isValid(stateWithErrors.foo)); const actualState = reducer( stateWithErrors, actions.resetValidity('test.foo', ['foo', 'bar'])); assert.containSubset( actualState.foo, { validity: {}, errors: {}, }); assert.isTrue(isValid(actualState.foo)); }); it('should reset the validity and errors of a form', () => { const stateWithErrors = reducer( undefined, actions.setErrors('test.foo', { bar: true, baz: true })); const stateWithMoreErrors = reducer( stateWithErrors, actions.setErrors('test.bar', { foo: true, baz: true })); assert.containSubset( stateWithMoreErrors.foo, { validity: { bar: false, baz: false }, errors: { bar: true, baz: true }, }); assert.isFalse(isValid(stateWithMoreErrors.foo)); assert.containSubset( stateWithMoreErrors.bar, { validity: { foo: false, baz: false }, errors: { foo: true, baz: true }, }); assert.isFalse(isValid(stateWithMoreErrors.bar)); const actualState = reducer( stateWithMoreErrors, actions.resetValidity('test')); assert.deepEqual(actualState.foo.validity, {}); assert.deepEqual(actualState.foo.errors, {}); assert.isTrue(isValid(actualState)); assert.containSubset( actualState.foo, { validity: {}, errors: {}, }); assert.isTrue(isValid(actualState.foo)); assert.containSubset( actualState.bar, { validity: {}, errors: {}, }); assert.isTrue(isValid(actualState.bar)); }); it('should be aliased to resetErrors()', () => { const stateWithErrors = reducer( undefined, actions.setErrors('test.foo', { bar: true, baz: true })); const actualState = reducer( stateWithErrors, actions.resetErrors('test.foo')); assert.deepEqual(actualState.foo.validity, {}); assert.deepEqual(actualState.foo.errors, {}); assert.isTrue(isValid(actualState.foo)); }); }); describe('asyncSetValidity() (thunk)', () => { it('should asynchronously call setValidity() action', (testDone) => { const reducer = formReducer('test'); const dispatch = action => { if (action.type === actionTypes.SET_VALIDITY) { const actual = reducer(undefined, action); assert.isFalse(isValid(actual)); assert.containSubset( actual.foo, { errors: { good: false, bad: true, }, }); testDone(); } }; const getState = () => ({ test: { foo: 5 }, }); const validator = (value, done) => done({ good: value > 4, bad: value > 5, }); actions.asyncSetValidity('test.foo', validator)(dispatch, getState); }); it('should work with forms to asynchronously call setValidity() action', testDone => { const reducer = formReducer('test'); const dispatch = action => { if (action.type === actionTypes.SET_VALIDITY) { const actual = reducer(undefined, action); assert.containSubset( actual.$form, { errors: { good: false, bad: true, }, }); assert.isFalse(isValid(actual)); testDone(); } }; const getState = () => ({ test: { foo: 5 }, }); const validator = ({ foo }, done) => done({ good: foo > 4, bad: foo > 5, }); actions.asyncSetValidity('test', validator)(dispatch, getState); }); it('should set validating to true for a field when validating, ' + 'and false when done validating', testDone => { const validatingStates = []; const executedActions = []; const reducer = formReducer('test'); const dispatch = (action) => { executedActions.push(action); const state = reducer(undefined, action); if (action.type === actionTypes.SET_VALIDATING) { validatingStates.push(action.validating); assert.equal(state.foo.validating, action.validating); } else if (action.type === actionTypes.SET_VALIDITY) { validatingStates.push(state.foo.validating); testDone(assert.deepEqual( validatingStates, [true, false])); } }; const getState = () => ({}); const validator = (_, done) => done(true); actions.asyncSetValidity('test.foo', validator)(dispatch, getState); }); it('should set validating to true for a form when validating, and false when done validating', testDone => { const validatingStates = []; const executedActions = []; const reducer = formReducer('test'); const dispatch = (action) => { executedActions.push(action); const state = reducer(undefined, action); if (action.type === actionTypes.SET_VALIDATING) { validatingStates.push(action.validating); assert.equal(state.$form.validating, action.validating); } else if (action.type === actionTypes.SET_VALIDITY) { validatingStates.push(state.$form.validating); testDone(assert.deepEqual( validatingStates, [true, false])); } }; const getState = () => ({}); const validator = (_, done) => done(true); actions.asyncSetValidity('test', validator)(dispatch, getState); }); }); describe('submit() (thunk)', () => { const submitPromise = value => new Promise((resolve, reject) => { if (value.valid) { return resolve(true); } return reject(value.errors); }); const mockStore = configureMockStore([thunk]); it('should exist', () => { assert.isFunction(actions.submit); }); it('should be able to resolve a form as valid', done => { const expectedActions = [ { type: actionTypes.SET_PENDING, pending: true, model: 'test' }, { type: actionTypes.BATCH, model: 'test', actions: [ { model: 'test', submitted: true, type: actionTypes.SET_SUBMITTED }, { model: 'test', type: actionTypes.SET_VALIDITY, validity: true }, ], }, ]; const store = mockStore(() => ({}), expectedActions, done); store.dispatch(actions.submit('test', submitPromise({ valid: true }))); }); it('should submit errors when form promise is rejected', done => { const errors = { foo: 'Foo is invalid', bar: 'Bar is also invalid', }; const expectedActions = [ { type: actionTypes.SET_PENDING, pending: true, model: 'test' }, { type: actionTypes.BATCH, model: 'test', actions: [ { type: actionTypes.SET_SUBMIT_FAILED, submitFailed: true, model: 'test', }, { type: actionTypes.SET_ERRORS, errors, async: true, model: 'test', }, ], }, ]; const store = mockStore(() => ({}), expectedActions, done); store.dispatch(actions.submit('test', submitPromise({ valid: false, errors, }))); }); it('should not submit if (optional) validators are invalid', (done) => { const store = createTestStore(testCreateStore({ testForm: formReducer('test'), }), done); store.when(actionTypes.SET_VALIDITY, (_, action) => { assert.containSubset(action, { model: 'test', validity: { foo: false }, }); }); const action = actions.submit('test', new Promise((r) => r(true)), { validators: { foo: () => false }, }); store.dispatch(action); }); it('should submit if (optional) validators are valid', (done) => { const store = createTestStore(testCreateStore({ testForm: formReducer('test'), }), done); store.when(actionTypes.SET_VALIDITY, (_, action) => { assert.containSubset(action, { model: 'test', validity: { foo: true }, }); }); store.when(actionTypes.SET_PENDING, () => true); store.when(actionTypes.SET_SUBMITTED, (_, action) => { assert.isTrue(action.submitted); }); const action = actions.submit('test', new Promise((r) => r(true)), { validators: { foo: () => true }, }); store.dispatch(action); }); it('should not submit if (optional) errors are invalid', (done) => { const store = createTestStore(testCreateStore({ testForm: formReducer('test'), }), done); store.when(actionTypes.SET_ERRORS, (_, action) => { assert.containSubset(action, { model: 'test', errors: { foo: true }, }); }); const action = actions.submit('test', new Promise((r) => r(true)), { errors: { foo: () => true }, }); store.dispatch(action); }); it('should submit if (optional) errors are valid', (done) => { const store = createTestStore(testCreateStore({ testForm: formReducer('test'), }), done); store.when(actionTypes.SET_VALIDITY, (_, action) => { assert.containSubset(action, { model: 'test', validity: { foo: true }, }); }); store.when(actionTypes.SET_PENDING, () => true); store.when(actionTypes.SET_SUBMITTED, (_, action) => { assert.isTrue(action.submitted); }); const action = actions.submit('test', new Promise((r) => r(true)), { errors: { foo: () => false }, }); store.dispatch(action); }); it('should add an intent to submit if no promise if given', (done) => { const store = createTestStore(testCreateStore({ testForm: formReducer('test'), }), done); store.when(actionTypes.ADD_INTENT, (_, action) => { assert.containSubset(action, { model: 'test', intent: { type: 'submit' }, }); }); const action = actions.submit('test'); store.dispatch(action); }); it('should capture thrown errors', (done) => { const store = createTestStore(testCreateStore({ testForm: formReducer('test'), }), done); store.when(actionTypes.SET_ERRORS, (_, action) => { assert.containSubset(action, { model: 'test', errors: 'Async error', }); }); const action = actions.submit('test', new Promise(() => { throw new Error('Async error'); })); store.dispatch(action); }); }); describe('validate() (thunk)', () => { const mockStore = configureMockStore([thunk]); it('should set the validity of a model with a validator function', (done) => { const store = mockStore( () => ({ test: { foo: 'bar' } }), [ { model: 'test.foo', type: actionTypes.SET_VALIDITY, validity: false }, { model: 'test.foo', type: actionTypes.SET_VALIDITY, validity: true }, ], done); store.dispatch(actions.validate('test.foo', (val) => val === 'invalid')); store.dispatch(actions.validate('test.foo', (val) => val === 'bar')); }); it('should set the validity of a model with a validation object', (done) => { const store = mockStore( () => ({ test: { foo: 'bar' } }), [ { model: 'test.foo', type: actionTypes.SET_VALIDITY, validity: { good: true, bad: false, }, }, ], done); const validators = { good: (val) => val === 'bar', bad: (val) => val === 'invalid', }; store.dispatch(actions.validate('test.foo', validators)); }); }); describe('validateErrors() (thunk)', () => { const mockStore = configureMockStore([thunk]); it('should set the errors of a model with an error validator function', (done) => { const store = mockStore( () => ({ test: { foo: 'bar' } }), [ { model: 'test.foo', type: actionTypes.SET_ERRORS, errors: 'Value is invalid' }, { model: 'test.foo', type: actionTypes.SET_ERRORS, errors: 'Value is invalid again' }, { model: 'test.foo', type: actionTypes.SET_ERRORS, errors: false }, ], done); store.dispatch(actions.validateErrors('test.foo', (val) => val !== 'valid' && 'Value is invalid')); store.dispatch(actions.validateErrors('test.foo', (val) => val !== 'valid' && 'Value is invalid again')); store.dispatch(actions.validateErrors('test.foo', (val) => val !== 'bar' && 'This should return false')); }); it('should allow any type of value as the error value', (done) => { const store = mockStore( () => ({ test: { foo: 'bar' } }), [ { model: 'test.foo', type: actionTypes.SET_ERRORS, errors: [ 'length', 'required', ], }, ], done); store.dispatch(actions.validateErrors('test.foo', (val) => val !== 'valid' && ['length', 'required'])); }); it('should set the errors of a model with an error object', (done) => { const store = mockStore( () => ({ test: { foo: 'bar' } }), [ { model: 'test.foo', type: actionTypes.SET_ERRORS, errors: { good: false, bad: 'Value is not valid', }, }, ], done); const errorValidators = { good: (val) => val !== 'bar' && 'This should not show', bad: (val) => val !== 'valid' && 'Value is not valid', }; store.dispatch(actions.validateErrors('test.foo', errorValidators)); }); }); describe('validateFields() (thunk)', () => { const mockStore = configureMockStore([thunk]); it('should set the validity of multiple fields in the same form', (done) => { const store = mockStore( () => ({ test: { foo: 'bar' } }), [ { fieldsValidity: { '': true, foo: true, foo_invalid: false, foo_valid: true, with_keys: { key_invalid: false, key_valid: true, }, }, model: 'test', type: actionTypes.SET_FIELDS_VALIDITY, options: {}, }, ], done); const action = actions.validateFields('test', { '': (val) => val.foo === 'bar', foo: (val) => val === 'bar', foo_valid: () => true, foo_invalid: () => false, with_keys: { key_valid: () => true, key_invalid: () => false, }, }); store.dispatch(action); }); }); describe('validateFieldsErrors() (thunk)', () => { const mockStore = configureMockStore([thunk]); it('should set the errors of multiple fields in the same form', (done) => { const store = mockStore( () => ({ test: { foo: 'invalid' } }), [ { fieldsValidity: { '': 'form is invalid', foo: 'foo is invalid', foo_invalid: 'foo_invalid is invalid', foo_valid: false, with_keys: { key_invalid: 'key_invalid is invalid', key_valid: false, }, }, model: 'test', type: actionTypes.SET_FIELDS_VALIDITY, options: { errors: true, }, }, ], done); const action = actions.validateFieldsErrors('test', { '': (val) => val.foo === 'invalid' && 'form is invalid', foo: (val) => val === 'invalid' && 'foo is invalid', foo_valid: () => false, foo_invalid: () => 'foo_invalid is invalid', with_keys: { key_valid: () => false, key_invalid: () => 'key_invalid is invalid', }, }); store.dispatch(action); }); }); describe('validSubmit() (thunk)', () => { it('should not submit a form if invalid', (done) => { const mockStore = configureMockStore([thunk]); const reducer = formReducer('test'); const expectedActions = [ { type: actionTypes.NULL, }, ]; const store = mockStore( () => ({ testForm: reducer(undefined, actions.setValidity('test', false)), }), expectedActions, done); const action = actions.validSubmit('test', new Promise((r) => r())); store.dispatch(action); }); it('should be able to resolve a form as valid', (done) => { const store = createTest