UNPKG

@ngbrown/react-redux-form

Version:

Create Forms Easily with React and Redux

754 lines (595 loc) 24.1 kB
/* eslint-disable */ import { assert } from 'chai'; import React from 'react'; import TestUtils from 'react-dom/test-utils'; import { combineReducers } from 'redux'; import { Provider } from 'react-redux'; import { testCreateStore, defaultTestContexts } from './utils'; import { Form as _Form, modelReducer as _modelReducer, formReducer as _formReducer, Field as _Field, Errors as _Errors, actions as _actions } from '../src'; import { Form as immutableForm, modelReducer as immutableModelReducer, formReducer as immutableFormReducer, Field as ImmutableField, Errors as ImmutableErrors, actions as immutableActions } from '../immutable'; const testContexts = { standard: { ...defaultTestContexts.standard, Form: _Form, modelReducer: _modelReducer, formReducer: _formReducer, Field: _Field, Errors: _Errors, actions: _actions, }, immutable: { ...defaultTestContexts.immutable, Form: immutableForm, modelReducer: immutableModelReducer, formReducer: immutableFormReducer, Field: ImmutableField, Errors: ImmutableErrors, actions: immutableActions, } }; 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 Errors = testContext.Errors; const actions = testContext.actions; const object = testContext.object; const get = testContext.get; const getInitialState = testContext.getInitialState; describe(`<Errors /> component (${testKey} context)`, () => { it('should exist', () => { assert.ok(Errors); }); describe('displaying errors from messages', () => { const initialState = getInitialState({foo: ''}); const store = testCreateStore({ testForm: formReducer('test', initialState), test: modelReducer('test', initialState), }); const form = TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" messages={{ required: 'This field is required', valid: 'This field is invalid', }} /> <Field model="test.foo" validators={{ required: (v) => v && v.length, valid: (v) => v === 'valid', }} > <input type="text"/> </Field> </form> </Provider> ); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); it('should display all errors', () => { const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 2); assert.equal(errors[0].innerHTML, 'This field is required'); assert.equal(errors[1].innerHTML, 'This field is invalid'); }); it('should display only relevant errors when validity changes', () => { const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); input.value = 'invalid'; TestUtils.Simulate.change(input); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 1); assert.equal(errors[0].innerHTML, 'This field is invalid'); }); it('should not display any errors for a valid field', () => { const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); input.value = 'valid'; TestUtils.Simulate.change(input); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 0); }); }); describe('displaying errors from field .errors', () => { const store = testCreateStore({ testForm: formReducer('test'), test: modelReducer('test'), }); const form = TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" messages={{ required: 'This field is required', }} /> <Field model="test.foo" validators={{ required: (v) => v && v.length, }} errors={{ valid: (v) => v !== 'valid' && 'This field is invalid', }} > <input type="text"/> </Field> </form> </Provider> ); it('should display all errors', () => { const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 2); assert.equal(errors[0].innerHTML, 'This field is required'); assert.equal(errors[1].innerHTML, 'This field is invalid'); }); }); describe('displaying errors from form .errors', () => { const initialState = getInitialState({foo: ''}); const store = testCreateStore({ testForm: formReducer('test', initialState), test: modelReducer('test', initialState), }); let formValid = false; const form = TestUtils.renderIntoDocument( <Provider store={store}> <Form model="test" validators={{ '': { foo: (model) => get(model, 'foo') && get(model, 'foo').length }, }} > <Errors model="test" messages={{ foo: 'This form is invalid', }} /> <Field model="test.foo"> <input type="text" /> </Field> </Form> </Provider> ); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); it('should display all form errors', () => { const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 1); assert.equal(errors[0].innerHTML, 'This form is invalid'); }); it('should not display form errors if form is valid', () => { input.value = 'testing'; TestUtils.Simulate.change(input); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 0); }); }); describe('displaying no errors', () => { const initialState = getInitialState({foo: ''}); const store = testCreateStore({ testForm: formReducer('test', initialState), test: modelReducer('test', initialState), }); const form = TestUtils.renderIntoDocument( <Provider store={store}> <Errors model="test"/> </Provider> ); it('should not render a component if there are no errors', () => { const divs = TestUtils.scryRenderedDOMComponentsWithTag(form, 'div'); assert.lengthOf(divs, 0); }); }); describe('displaying custom messages', () => { const initialState = getInitialState({foo: ''}); const store = testCreateStore({ testForm: formReducer('test', initialState), test: modelReducer('test', initialState), }); const form = TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" messages={{ length: (val) => `${val && val.length} chars is too short`, doNotShow: () => false, }} /> <Field model="test.foo" validators={{ length: (v) => v && v.length && v.length > 5 ? {length: 5} : false, doNotShow: () => false, }} > <input type="text"/> </Field> </form> </Provider> ); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); it('should return messages from functions called with the model value', () => { input.value = 'four'; TestUtils.Simulate.change(input); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 1); assert.equal(errors[0].innerHTML, '4 chars is too short'); }); it('should not show messages when functions return falsey values', () => { TestUtils.Simulate.change(input); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.property(store.getState().testForm.foo.errors, 'doNotShow'); assert.isTrue(store.getState().testForm.foo.errors.doNotShow); assert.lengthOf(errors, 1); }); }); describe('displaying custom error messages', () => { const initialState = getInitialState({foo: ''}); const store = testCreateStore({ testForm: formReducer('test', initialState), test: modelReducer('test', initialState), }); const form = TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" messages={{ length: (val, {length}) => `${val && val.length} chars is too short (must be at least ${length} chars)`, doNotShow: () => false, }} /> <Field model="test.foo" errors={{ length: (v) => v && v.length && v.length > 5 ? false : {length: 5}, doNotShow: () => false, }} > <input type="text"/> </Field> </form> </Provider> ); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); it('should return messages from functions called with the model value', () => { input.value = 'four'; TestUtils.Simulate.change(input); const errors = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(errors, 1); assert.equal(errors[0].innerHTML, '4 chars is too short (must be at least 5 chars)'); }); }); describe('the "show" prop', () => { function renderErrorsWithShow(show) { const store = testCreateStore({ testForm: formReducer('test', object), test: modelReducer('test'), }); return TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" messages={{ required: 'This field is required', }} show={ show } /> <Field model="test.foo" validators={{ required: (v) => v && v.length, }} component="section" > <input type="text"/> </Field> </form> </Provider> ); } it('should support a function that shows based on field value', () => { const showFn = (field) => field.focus; const form = renderErrorsWithShow(showFn); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0); TestUtils.Simulate.focus(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 1); TestUtils.Simulate.blur(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0); }); it('should support a function that shows based on field and form value', () => { const store = testCreateStore({ testForm: formReducer('test', object), test: modelReducer('test'), }); const showFn = (field, form) => field.focus || form.submitFailed; const form = TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" messages={{ required: 'This field is required', }} show={ showFn } /> <Field model="test.foo" validators={{ required: (v) => v && v.length, }} component="section" > <input type="text"/> </Field> </form> </Provider> ); const formElement = TestUtils.findRenderedDOMComponentWithTag(form, 'form'); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0); TestUtils.Simulate.focus(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 1); TestUtils.Simulate.blur(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0); store.dispatch(actions.setSubmitFailed('test')); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 1, 'form submit failed'); }); it('should support a boolean and show if truthy', () => { const form = renderErrorsWithShow(true); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 1); }); it('should support a boolean and not show if falsey', () => { const form = renderErrorsWithShow(false); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'div'), 0); }); it('should support a string and show if that field property is truthy', () => { const form = renderErrorsWithShow('focus'); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0); TestUtils.Simulate.focus(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 1); TestUtils.Simulate.blur(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0); }); it('should support an object and show if the properties match', () => { const form = renderErrorsWithShow({ focus: true, touched: true, }); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0, 'not focused yet'); TestUtils.Simulate.focus(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0, 'not touched yet'); TestUtils.Simulate.blur(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0, 'touched but not focused'); TestUtils.Simulate.focus(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 1, 'touched and focused'); TestUtils.Simulate.blur(input); assert.lengthOf(TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'), 0, 'touched but not focused'); }); }); describe('the "wrapper" prop', () => { function renderErrorsWithWrapper(wrapper, props) { const store = testCreateStore({ testForm: formReducer('test', object), test: modelReducer('test'), }); return TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" wrapper={wrapper} messages={{ foo: 'foo error', bar: 'bar error', }} {...props} /> <Field model="test.foo" validators={{ foo: () => false, bar: () => false, }} component="main" > <input type="text"/> </Field> </form> </Provider> ); } it('should render a <div> wrapper by default', () => { const form = renderErrorsWithWrapper(); const wrapper = TestUtils.scryRenderedDOMComponentsWithTag(form, 'div'); assert.lengthOf(wrapper, 1); assert.equal(wrapper[0].childNodes.length, 2); }); it('should render a specified HTML element wrapper', () => { const form = renderErrorsWithWrapper('section'); const wrapper = TestUtils.scryRenderedDOMComponentsWithTag(form, 'section'); assert.lengthOf(wrapper, 1); assert.equal(wrapper[0].childNodes.length, 2); }); it('should render a specified React component (class)', () => { class Wrapper extends React.Component { render() { return <main className="wrapper">{ this.props.children }</main>; } } const form = renderErrorsWithWrapper(Wrapper); const wrapper = TestUtils.scryRenderedDOMComponentsWithClass(form, 'wrapper'); assert.lengthOf(wrapper, 1); assert.equal(wrapper[0].childNodes.length, 2); }); it('should render a specified React component (function) with error props', () => { function Wrapper(props) { assert.property(props, 'model'); assert.property(props, 'modelValue'); assert.property(props, 'fieldValue'); assert.property(props, 'messages'); assert.property(props, 'show'); assert.property(props, 'component'); assert.equal(props.model, 'test.foo'); return <div className="wrapper">{props.children}</div>; } const form = renderErrorsWithWrapper(Wrapper); const wrapper = TestUtils.scryRenderedDOMComponentsWithClass(form, 'wrapper'); assert.lengthOf(wrapper, 1); assert.equal(wrapper[0].childNodes.length, 2); }); it('should render a specified React component with non-proprietary props', () => { function Wrapper(props) { assert.property(props, 'className'); return <div className={props.className}>{props.children}</div>; } const props = {className: 'custom-class'}; const form = renderErrorsWithWrapper(Wrapper, props); const wrapper = TestUtils.scryRenderedDOMComponentsWithClass(form, 'custom-class'); assert.lengthOf(wrapper, 1); assert.equal(wrapper[0].childNodes.length, 2); }); }); describe('the "component" prop', () => { function renderErrorsWithComponent(component) { const store = testCreateStore({ testForm: formReducer('test', object), test: modelReducer('test'), }); return TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="test.foo" component={component} messages={{ foo: 'foo error', bar: 'bar error', }} /> <Field model="test.foo" validators={{ foo: () => false, bar: () => false, }} > <input type="text"/> </Field> </form> </Provider> ); } it('should render a <span> component by default', () => { const form = renderErrorsWithComponent(); const components = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span'); assert.lengthOf(components, 2); assert.equal(components[0].innerHTML, 'foo error'); assert.equal(components[1].innerHTML, 'bar error'); }); it('should render a specified HTML element component', () => { const form = renderErrorsWithComponent('p'); const components = TestUtils.scryRenderedDOMComponentsWithTag(form, 'p'); assert.lengthOf(components, 2); assert.equal(components[0].innerHTML, 'foo error'); assert.equal(components[1].innerHTML, 'bar error'); }); it('should render a specified React component (class)', () => { class ErrorComponent extends React.Component { render() { return <main className="component">{ this.props.children }</main>; } } const form = renderErrorsWithComponent(ErrorComponent); const components = TestUtils.scryRenderedDOMComponentsWithClass(form, 'component'); assert.lengthOf(components, 2); assert.equal(components[0].innerHTML, 'foo error'); assert.equal(components[1].innerHTML, 'bar error'); }); it('should render a specified React component (function) with error props', () => { function ErrorComponent(props) { assert.property(props, 'model'); assert.property(props, 'modelValue'); assert.property(props, 'fieldValue'); assert.equal(props.model, 'test.foo'); return <div className="component">{props.children}</div>; } const form = renderErrorsWithComponent(ErrorComponent); const components = TestUtils.scryRenderedDOMComponentsWithClass(form, 'component'); assert.lengthOf(components, 2); assert.equal(components[0].innerHTML, 'foo error'); assert.equal(components[1].innerHTML, 'bar error'); }); }); describe('deep model paths', () => { it('should work with deep model paths', () => { const store = testCreateStore(({ forms: combineReducers({ testForm: formReducer('forms.test', object), test: modelReducer('forms.test'), }), })); const form = TestUtils.renderIntoDocument( <Provider store={store}> <form> <Errors model="forms.test.foo" messages={{ required: 'This field is required', }} /> <Field model="forms.test.foo" validators={{ required: (v) => v && v.length, }} > <input type="text"/> </Field> </form> </Provider> ); const spans = TestUtils.scryRenderedDOMComponentsWithTag(form, 'span') assert.lengthOf(spans, 1); assert.equal(spans[0].innerHTML, 'This field is required') }); }); describe('single string error messages', () => { it('should work with single string error messages', () => { const store = testCreateStore({ testForm: formReducer('test', object), test: modelReducer('test'), }); const form = TestUtils.renderIntoDocument( <Provider store={store}> <Errors model="test" messages={{ foo: 'foo', bar: 'bar', }}/> </Provider> ); store.dispatch(actions.setErrors('test', 'this is a single error message')); let error; assert.doesNotThrow(() => error = TestUtils .findRenderedDOMComponentWithTag(form, 'span')); assert.equal(error.innerHTML, 'this is a single error message'); }); }); }); });