From 6353d5e410e8ec2a2b6642cc85d146d7285b642e Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 28 Mar 2019 13:02:04 -0400 Subject: [PATCH] update tests for org add/edit refactor --- .../components/OrganizationForm.test.jsx | 240 ++++++++++++++++ .../Organization/OrganizationEdit.test.jsx | 257 +++++------------- .../screens/OrganizationAdd.test.jsx | 210 ++++++-------- __tests__/util/validators.test.js | 34 +++ .../components/OrganizationForm.jsx | 2 +- .../screens/Organization/OrganizationEdit.jsx | 1 + .../Organizations/screens/OrganizationAdd.jsx | 11 +- src/util/validators.js | 3 +- 8 files changed, 424 insertions(+), 334 deletions(-) create mode 100644 __tests__/pages/Organizations/components/OrganizationForm.test.jsx create mode 100644 __tests__/util/validators.test.js diff --git a/__tests__/pages/Organizations/components/OrganizationForm.test.jsx b/__tests__/pages/Organizations/components/OrganizationForm.test.jsx new file mode 100644 index 0000000000..7c1d32dbc0 --- /dev/null +++ b/__tests__/pages/Organizations/components/OrganizationForm.test.jsx @@ -0,0 +1,240 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { MemoryRouter } from 'react-router-dom'; +import { I18nProvider } from '@lingui/react'; +import { ConfigContext } from '../../../../src/context'; +import OrganizationForm from '../../../../src/pages/Organizations/components/OrganizationForm'; + +const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +describe('', () => { + let api; + + const mockData = { + id: 1, + name: 'Foo', + description: 'Bar', + custom_virtualenv: 'Fizz', + related: { + instance_groups: '/api/v2/organizations/1/instance_groups' + } + }; + + beforeEach(() => { + api = { + getInstanceGroups: jest.fn(), + }; + }); + + test('should request related instance groups from api', () => { + const mockInstanceGroups = [ + { name: 'One', id: 1 }, + { name: 'Two', id: 2 } + ]; + api.getOrganizationInstanceGroups = jest.fn(() => ( + Promise.resolve({ data: { results: mockInstanceGroups } }) + )); + mount( + + + + + + ).find('OrganizationForm'); + + expect(api.getOrganizationInstanceGroups).toHaveBeenCalledTimes(1); + }); + + test('componentDidMount should set instanceGroups to state', async () => { + const mockInstanceGroups = [ + { name: 'One', id: 1 }, + { name: 'Two', id: 2 } + ]; + api.getOrganizationInstanceGroups = jest.fn(() => ( + Promise.resolve({ data: { results: mockInstanceGroups } }) + )); + const wrapper = mount( + + + + + + ).find('OrganizationForm'); + + await wrapper.instance().componentDidMount(); + expect(wrapper.state().instanceGroups).toEqual(mockInstanceGroups); + }); + + test('changing instance group successfully sets instanceGroups state', () => { + const wrapper = mount( + + + + + + ).find('OrganizationForm'); + + const lookup = wrapper.find('InstanceGroupsLookup'); + expect(lookup.length).toBe(1); + + lookup.prop('onChange')([ + { + id: 1, + name: 'foo' + } + ], 'instanceGroups'); + expect(wrapper.state().instanceGroups).toEqual([ + { + id: 1, + name: 'foo' + } + ]); + }); + + test('changing inputs should update form values', () => { + const wrapper = mount( + + + + + + ).find('OrganizationForm'); + + const form = wrapper.find('Formik'); + wrapper.find('input#edit-org-form-name').simulate('change', { + target: { value: 'new foo', name: 'name' } + }); + expect(form.state('values').name).toEqual('new foo'); + wrapper.find('input#edit-org-form-description').simulate('change', { + target: { value: 'new bar', name: 'description' } + }); + expect(form.state('values').description).toEqual('new bar'); + }); + + test('AnsibleSelect component renders if there are virtual environments', () => { + const config = { + custom_virtualenvs: ['foo', 'bar'], + }; + const wrapper = mount( + + + + + + + + ); + expect(wrapper.find('FormSelect')).toHaveLength(1); + expect(wrapper.find('FormSelectOption')).toHaveLength(2); + }); + + test('calls handleSubmit when form submitted', async () => { + const handleSubmit = jest.fn(); + const wrapper = mount( + + + + + + ).find('OrganizationForm'); + expect(wrapper.prop('handleSubmit')).not.toHaveBeenCalled(); + wrapper.find('button[aria-label="Save"]').simulate('click'); + await sleep(1); + expect(handleSubmit).toHaveBeenCalledWith({ + name: 'Foo', + description: 'Bar', + custom_virtualenv: 'Fizz', + }, [], []); + }); + + test('handleSubmit associates and disassociates instance groups', async () => { + const mockInstanceGroups = [ + { name: 'One', id: 1 }, + { name: 'Two', id: 2 } + ]; + api.getOrganizationInstanceGroups = jest.fn(() => ( + Promise.resolve({ data: { results: mockInstanceGroups } }) + )); + const mockDataForm = { + name: 'Foo', + description: 'Bar', + custom_virtualenv: 'Fizz', + }; + const handleSubmit = jest.fn(); + api.updateOrganizationDetails = jest.fn().mockResolvedValue(1, mockDataForm); + api.associateInstanceGroup = jest.fn().mockResolvedValue('done'); + api.disassociate = jest.fn().mockResolvedValue('done'); + const wrapper = mount( + + + + + + ).find('OrganizationForm'); + + await wrapper.instance().componentDidMount(); + + wrapper.find('InstanceGroupsLookup').prop('onChange')([ + { name: 'One', id: 1 }, + { name: 'Three', id: 3 } + ], 'instanceGroups'); + + wrapper.find('button[aria-label="Save"]').simulate('click'); + await sleep(0); + expect(handleSubmit).toHaveBeenCalledWith(mockDataForm, [3], [2]); + }); + + test('calls "handleCancel" when Cancel button is clicked', () => { + const handleCancel = jest.fn(); + const wrapper = mount( + + + + + + ); + expect(handleCancel).not.toHaveBeenCalled(); + wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); + expect(handleCancel).toBeCalled(); + }); +}); diff --git a/__tests__/pages/Organizations/screens/Organization/OrganizationEdit.test.jsx b/__tests__/pages/Organizations/screens/Organization/OrganizationEdit.test.jsx index aed7c7687f..f67dc8780d 100644 --- a/__tests__/pages/Organizations/screens/Organization/OrganizationEdit.test.jsx +++ b/__tests__/pages/Organizations/screens/Organization/OrganizationEdit.test.jsx @@ -2,8 +2,9 @@ import React from 'react'; import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; import { I18nProvider } from '@lingui/react'; -import { ConfigContext } from '../../../../../src/context'; -import OrganizationEdit from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit'; +import OrganizationEdit, { OrganizationEditNoRouter } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit'; + +const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); describe('', () => { let api; @@ -21,226 +22,90 @@ describe('', () => { beforeEach(() => { api = { getInstanceGroups: jest.fn(), + updateOrganizationDetails: jest.fn(), + associateInstanceGroup: jest.fn(), + disassociate: jest.fn(), }; }); - test('should request related instance groups from api', () => { - const mockInstanceGroups = [ - { name: 'One', id: 1 }, - { name: 'Two', id: 2 } - ]; - api.getOrganizationInstanceGroups = jest.fn(() => ( - Promise.resolve({ data: { results: mockInstanceGroups } }) - )); - mount( - - - - - - ).find('OrganizationEdit'); - - expect(api.getOrganizationInstanceGroups).toHaveBeenCalledTimes(1); - }); - - test('componentDidMount should set instanceGroups to state', async () => { - const mockInstanceGroups = [ - { name: 'One', id: 1 }, - { name: 'Two', id: 2 } - ]; - api.getOrganizationInstanceGroups = jest.fn(() => ( - Promise.resolve({ data: { results: mockInstanceGroups } }) - )); - const wrapper = mount( - - - - - - ).find('OrganizationEdit'); - - await wrapper.instance().componentDidMount(); - expect(wrapper.state().form.instanceGroups.value).toEqual(mockInstanceGroups); - }); - - test('changing instance group successfully sets instanceGroups state', () => { + test('handleSubmit should call api update', () => { const wrapper = mount( - - - ).find('OrganizationEdit'); - - const lookup = wrapper.find('InstanceGroupsLookup'); - expect(lookup.length).toBe(1); - - lookup.prop('onChange')([ - { - id: 1, - name: 'foo' - } - ], 'instanceGroups'); - expect(wrapper.state().form.instanceGroups.value).toEqual([ - { - id: 1, - name: 'foo' - } - ]); - }); - - test('calls "handleFieldChange" when input values change', () => { - const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'handleFieldChange'); - const wrapper = mount( - - - - - - ).find('OrganizationEdit'); - - expect(spy).not.toHaveBeenCalled(); - wrapper.instance().handleFieldChange('foo', { target: { name: 'name' } }); - wrapper.instance().handleFieldChange('bar', { target: { name: 'description' } }); - expect(spy).toHaveBeenCalledTimes(2); - }); - - test('AnsibleSelect component renders if there are virtual environments', () => { - const config = { - custom_virtualenvs: ['foo', 'bar'], - }; - const wrapper = mount( - - - - - - - - ); - expect(wrapper.find('FormSelect')).toHaveLength(1); - expect(wrapper.find('FormSelectOption')).toHaveLength(2); - }); - - test('calls handleSubmit when Save button is clicked', () => { - const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'handleSubmit'); - const wrapper = mount( - - - ); - expect(spy).not.toHaveBeenCalled(); - wrapper.find('button[aria-label="Save"]').prop('onClick')(); - expect(spy).toBeCalled(); + + const updatedOrgData = { + name: 'new name', + description: 'new description', + custom_virtualenv: 'Buzz', + }; + wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [], []); + + expect(api.updateOrganizationDetails).toHaveBeenCalledWith( + 1, + updatedOrgData + ); }); test('handleSubmit associates and disassociates instance groups', async () => { - const mockInstanceGroups = [ - { name: 'One', id: 1 }, - { name: 'Two', id: 2 } - ]; - api.getOrganizationInstanceGroups = jest.fn(() => ( - Promise.resolve({ data: { results: mockInstanceGroups } }) - )); - const mockDataForm = { - name: 'Foo', - description: 'Bar', - custom_virtualenv: 'Fizz', - }; - api.updateOrganizationDetails = jest.fn().mockResolvedValue(1, mockDataForm); - api.associateInstanceGroup = jest.fn().mockResolvedValue('done'); - api.disassociate = jest.fn().mockResolvedValue('done'); - const wrapper = mount( - - - - - - ).find('OrganizationEdit'); - - await wrapper.instance().componentDidMount(); - - wrapper.find('InstanceGroupsLookup').prop('onChange')([ - { name: 'One', id: 1 }, - { name: 'Three', id: 3 } - ], 'instanceGroups'); - - await wrapper.instance().handleSubmit(); - expect(api.updateOrganizationDetails).toHaveBeenCalledWith(1, mockDataForm); - expect(api.associateInstanceGroup).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 3); - expect(api.associateInstanceGroup).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1); - expect(api.associateInstanceGroup).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 2); - - expect(api.disassociate).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 2); - expect(api.disassociate).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1); - expect(api.disassociate).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 3); - }); - - test('calls "handleCancel" when Cancel button is clicked', () => { - const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'handleCancel'); const wrapper = mount( ); - expect(spy).not.toHaveBeenCalled(); + + const updatedOrgData = { + name: 'new name', + description: 'new description', + custom_virtualenv: 'Buzz', + }; + wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [3, 4], [2]); + await sleep(1); + + expect(api.associateInstanceGroup).toHaveBeenCalledWith( + '/api/v2/organizations/1/instance_groups', + 3 + ); + expect(api.associateInstanceGroup).toHaveBeenCalledWith( + '/api/v2/organizations/1/instance_groups', + 4 + ); + expect(api.disassociate).toHaveBeenCalledWith( + '/api/v2/organizations/1/instance_groups', + 2 + ); + }); + + test('should navigate to organization detail when cancel is clicked', () => { + const history = { + push: jest.fn(), + }; + const wrapper = mount( + + + + + + ); + + expect(history.push).not.toHaveBeenCalled(); wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); - expect(spy).toBeCalled(); + + expect(history.push).toHaveBeenCalledWith('/organizations/1'); }); }); diff --git a/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx b/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx index 954fa1f2fa..95eb5d631c 100644 --- a/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx +++ b/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx @@ -1,9 +1,11 @@ import React from 'react'; import { mount } from 'enzyme'; -import { MemoryRouter, Router } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; import { I18nProvider } from '@lingui/react'; import { ConfigContext } from '../../../../src/context'; -import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd'; +import OrganizationAdd, { OrganizationAddNoRouter } from '../../../../src/pages/Organizations/screens/OrganizationAdd'; + +const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); describe('', () => { let api; @@ -11,184 +13,140 @@ describe('', () => { beforeEach(() => { api = { getInstanceGroups: jest.fn(), + createOrganization: jest.fn(), + associateInstanceGroup: jest.fn(), + disassociate: jest.fn(), }; }); - test('initially renders succesfully', () => { - mount( - - - - - - ); - }); - - test('calls "handleFieldChange" when input values change', () => { - const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'handleFieldChange'); + test('handleSubmit should post to api', () => { const wrapper = mount( ); - expect(spy).not.toHaveBeenCalled(); - wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } }); - wrapper.find('input#add-org-form-description').simulate('change', { target: { value: 'bar' } }); - expect(spy).toHaveBeenCalledTimes(2); + + const updatedOrgData = { + name: 'new name', + description: 'new description', + custom_virtualenv: 'Buzz', + }; + wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [], []); + + expect(api.createOrganization).toHaveBeenCalledWith(updatedOrgData); }); - test('calls "handleSubmit" when Save button is clicked', () => { - const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'handleSubmit'); + test('should navigate to organizations list when cancel is clicked', () => { + const history = { + push: jest.fn(), + }; const wrapper = mount( - ); - expect(spy).not.toHaveBeenCalled(); - wrapper.find('button[aria-label="Save"]').prop('onClick')(); - expect(spy).toBeCalled(); - }); - test('calls "handleCancel" when Cancel button is clicked', () => { - const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'handleCancel'); - const wrapper = mount( - - - - - - ); - expect(spy).not.toHaveBeenCalled(); + expect(history.push).not.toHaveBeenCalled(); wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); - expect(spy).toBeCalled(); + + expect(history.push).toHaveBeenCalledWith('/organizations'); }); - test('calls "handleCancel" when close button (x) is clicked', () => { + test('should navigate to organizations list when close (x) is clicked', () => { + const history = { + push: jest.fn(), + }; const wrapper = mount( - + - ); - const history = wrapper.find(Router).prop('history'); - expect(history.length).toBe(1); - expect(history.location.pathname).toEqual('/organizations/add'); + + expect(history.push).not.toHaveBeenCalled(); wrapper.find('button[aria-label="Close"]').prop('onClick')(); - expect(history.length).toBe(2); - expect(history.location.pathname).toEqual('/organizations'); + + expect(history.push).toHaveBeenCalledWith('/organizations'); }); - test('Successful form submission triggers redirect', (done) => { - const handleSuccess = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'handleSuccess'); - const mockedResp = { data: { id: 1, related: { instance_groups: '/bar' } } }; - api.createOrganization = jest.fn().mockResolvedValue(mockedResp); api.associateInstanceGroup = jest.fn().mockResolvedValue('done'); + test('successful form submission should trigger redirect', async () => { + const history = { + push: jest.fn(), + }; + const orgData = { + name: 'new name', + description: 'new description', + custom_virtualenv: 'Buzz', + }; + api.createOrganization.mockReturnValueOnce({ + data: { + id: 5, + related: { + instance_groups: '/bar', + }, + ...orgData, + } + }); const wrapper = mount( - + ); - wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } }); - wrapper.find('button[aria-label="Save"]').prop('onClick')(); - setImmediate(() => { - expect(handleSuccess).toHaveBeenCalled(); - done(); - }); + + wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [], []); + await sleep(0); + + expect(history.push).toHaveBeenCalledWith('/organizations/5'); }); - test('changing instance groups successfully sets instanceGroups state', () => { + test('handleSubmit should post instance groups', async () => { const wrapper = mount( - + - ).find('OrganizationAdd'); + ); - wrapper.find('InstanceGroupsLookup').prop('onChange')([ - { - id: 1, - name: 'foo' - } - ], 'instanceGroups'); - expect(wrapper.state('instanceGroups')).toEqual([ - { - id: 1, - name: 'foo' - } - ]); - }); - - test('handleFieldChange successfully sets custom_virtualenv state', () => { - const wrapper = mount( - - - - - - ).find('OrganizationAdd'); - wrapper.instance().handleFieldChange('fooBar', { target: { name: 'custom_virtualenv' } }); - expect(wrapper.state('custom_virtualenv')).toBe('fooBar'); - }); - - test('handleSubmit posts instance groups from selectedInstanceGroups', async () => { - api.createOrganization = jest.fn().mockResolvedValue({ + const orgData = { + name: 'new name', + description: 'new description', + custom_virtualenv: 'Buzz', + }; + api.createOrganization.mockReturnValueOnce({ data: { - id: 1, - name: 'mock org', + id: 5, related: { - instance_groups: '/api/v2/organizations/1/instance_groups' - } + instance_groups: '/api/v2/organizations/5/instance_groups', + }, + ...orgData, } }); - api.associateInstanceGroup = jest.fn().mockResolvedValue('done'); - const wrapper = mount( - - - - - - ).find('OrganizationAdd'); - wrapper.setState({ - name: 'mock org', - instanceGroups: [{ - id: 1, - name: 'foo' - }] - }); - await wrapper.instance().handleSubmit(); - expect(api.createOrganization).toHaveBeenCalledWith({ - custom_virtualenv: '', - description: '', - name: 'mock org' - }); - expect(api.associateInstanceGroup).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1); + wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []); + await sleep(0); + + expect(api.associateInstanceGroup) + .toHaveBeenCalledWith('/api/v2/organizations/5/instance_groups', 3); }); test('AnsibleSelect component renders if there are virtual environments', () => { diff --git a/__tests__/util/validators.test.js b/__tests__/util/validators.test.js new file mode 100644 index 0000000000..d197c61a21 --- /dev/null +++ b/__tests__/util/validators.test.js @@ -0,0 +1,34 @@ +import { required, maxLength } from '../../src/util/validators'; + +describe('validators', () => { + test('required returns undefined if value given', () => { + expect(required()('some value')).toBeUndefined(); + expect(required('oops')('some value')).toBeUndefined(); + }); + + test('required returns default message if value missing', () => { + expect(required()('')).toEqual('This field must not be blank'); + }); + + test('required returns custom message if value missing', () => { + expect(required('oops')('')).toEqual('oops'); + }); + + test('required interprets white space as empty value', () => { + expect(required()(' ')).toEqual('This field must not be blank'); + expect(required()('\t')).toEqual('This field must not be blank'); + }); + + test('maxLength accepts value below max', () => { + expect(maxLength(10)('snazzy')).toBeUndefined(); + }); + + test('maxLength accepts value equal to max', () => { + expect(maxLength(10)('abracadbra')).toBeUndefined(); + }); + + test('maxLength rejects value above max', () => { + expect(maxLength(8)('abracadbra')) + .toEqual('This field must not exceed 8 characters'); + }); +}); diff --git a/src/pages/Organizations/components/OrganizationForm.jsx b/src/pages/Organizations/components/OrganizationForm.jsx index 5588f6c1cf..a197982e43 100644 --- a/src/pages/Organizations/components/OrganizationForm.jsx +++ b/src/pages/Organizations/components/OrganizationForm.jsx @@ -65,7 +65,7 @@ class OrganizationForm extends Component { this.setState({ instanceGroups }); } - async handleSubmit (values) { + handleSubmit (values) { const { handleSubmit } = this.props; const { instanceGroups, initialInstanceGroups } = this.state; diff --git a/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx b/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx index f4de4309d0..33eabfcb33 100644 --- a/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx +++ b/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx @@ -85,4 +85,5 @@ OrganizationEdit.contextTypes = { custom_virtualenvs: PropTypes.arrayOf(PropTypes.string) }; +export { OrganizationEdit as OrganizationEditNoRouter }; export default withRouter(OrganizationEdit); diff --git a/src/pages/Organizations/screens/OrganizationAdd.jsx b/src/pages/Organizations/screens/OrganizationAdd.jsx index ec69bbf9be..b750f028bc 100644 --- a/src/pages/Organizations/screens/OrganizationAdd.jsx +++ b/src/pages/Organizations/screens/OrganizationAdd.jsx @@ -19,8 +19,6 @@ class OrganizationAdd extends React.Component { constructor (props) { super(props); - this.handleFieldChange = this.handleFieldChange.bind(this); - this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); this.handleSuccess = this.handleSuccess.bind(this); @@ -30,14 +28,6 @@ class OrganizationAdd extends React.Component { }; } - handleFieldChange (val, evt) { - this.setState({ [evt.target.name]: val || evt.target.value }); - } - - handleInstanceGroupsChange (val, targetName) { - this.setState({ [targetName]: val }); - } - async handleSubmit (values, groupsToAssociate) { const { api } = this.props; try { @@ -114,4 +104,5 @@ OrganizationAdd.contextTypes = { custom_virtualenvs: PropTypes.arrayOf(PropTypes.string) }; +export { OrganizationAdd as OrganizationAddNoRouter }; export default withRouter(OrganizationAdd); diff --git a/src/util/validators.js b/src/util/validators.js index 4e0f95e25b..939bb1bdec 100644 --- a/src/util/validators.js +++ b/src/util/validators.js @@ -11,7 +11,8 @@ export function required (message) { export function maxLength (max) { return value => { - if (value.trim() > max) { + if (value.trim().length + > max) { return i18nMark(`This field must not exceed ${max} characters`); } return undefined;