diff --git a/packages/venia-concept/src/components/CreateAccount/__mocks__/asyncValidators.js b/packages/venia-concept/src/components/CreateAccount/__mocks__/asyncValidators.js new file mode 100644 index 0000000000..22e6ba2526 --- /dev/null +++ b/packages/venia-concept/src/components/CreateAccount/__mocks__/asyncValidators.js @@ -0,0 +1 @@ +export const validateEmail = jest.fn(); diff --git a/packages/venia-concept/src/components/CreateAccount/__mocks__/validators.js b/packages/venia-concept/src/components/CreateAccount/__mocks__/validators.js deleted file mode 100644 index 3bab06e34e..0000000000 --- a/packages/venia-concept/src/components/CreateAccount/__mocks__/validators.js +++ /dev/null @@ -1,12 +0,0 @@ -const validators = new Map(); -const asyncValidators = new Map(); - -const keys = ['confirm', 'email', 'firstName', 'lastName', 'password']; - -for (const key of keys) { - validators.set(key, jest.fn()); -} - -asyncValidators.set('email', jest.fn()); - -export { asyncValidators, validators }; diff --git a/packages/venia-concept/src/components/CreateAccount/__tests__/__snapshots__/createAccount.spec.js.snap b/packages/venia-concept/src/components/CreateAccount/__tests__/__snapshots__/createAccount.spec.js.snap index c570f97076..4120ddc143 100644 --- a/packages/venia-concept/src/components/CreateAccount/__tests__/__snapshots__/createAccount.spec.js.snap +++ b/packages/venia-concept/src/components/CreateAccount/__tests__/__snapshots__/createAccount.spec.js.snap @@ -92,6 +92,7 @@ exports[`renders the correct tree 1`] = `
+ Password
+ Confirm Password { const tree = createTestInstance().toJSON(); @@ -34,22 +33,6 @@ test('attaches the submit handler', () => { expect(onSubmit).toBe(instance.handleSubmit); }); -test('executes validators on submit', async () => { - const { root } = createTestInstance(); - - const form = root.findByType(Form); - const { formApi } = form.instance; - - // touch fields, call validators, call onSubmit - act(() => { - formApi.submitForm(); - }); - - for (const validator of validators.values()) { - expect(validator).toHaveBeenCalledTimes(2); - } -}); - test('calls onSubmit if validation passes', async () => { const { root } = createTestInstance( diff --git a/packages/venia-concept/src/components/CreateAccount/asyncValidators.js b/packages/venia-concept/src/components/CreateAccount/asyncValidators.js new file mode 100644 index 0000000000..fb843b474f --- /dev/null +++ b/packages/venia-concept/src/components/CreateAccount/asyncValidators.js @@ -0,0 +1,22 @@ +import { RestApi } from '@magento/peregrine'; + +const { request } = RestApi.Magento2; + +export const validateEmail = async value => { + try { + const body = { + customerEmail: value, + website_id: null + }; + + // response is a boolean + const available = await request('/rest/V1/customers/isEmailAvailable', { + method: 'POST', + body: JSON.stringify(body) + }); + + return !available ? 'This email address is not available.' : null; + } catch (error) { + throw 'An error occurred while looking up this email address.'; + } +}; diff --git a/packages/venia-concept/src/components/CreateAccount/createAccount.js b/packages/venia-concept/src/components/CreateAccount/createAccount.js index 12ee0f4c65..36033eb262 100644 --- a/packages/venia-concept/src/components/CreateAccount/createAccount.js +++ b/packages/venia-concept/src/components/CreateAccount/createAccount.js @@ -7,7 +7,16 @@ import Button from 'src/components/Button'; import Checkbox from 'src/components/Checkbox'; import Field from 'src/components/Field'; import TextInput from 'src/components/TextInput'; -import { validators } from './validators'; + +import combine from 'src/util/combineValidators'; +import { + validateEmail, + isRequired, + validatePassword, + validateConfirmPassword, + hasLengthAtLeast +} from 'src/util/formValidators'; + import defaultClasses from './createAccount.css'; class CreateAccount extends Component { @@ -81,7 +90,7 @@ class CreateAccount extends Component { @@ -89,7 +98,7 @@ class CreateAccount extends Component { @@ -97,24 +106,31 @@ class CreateAccount extends Component { - + - + diff --git a/packages/venia-concept/src/components/CreateAccount/validators.js b/packages/venia-concept/src/components/CreateAccount/validators.js deleted file mode 100644 index dc86656853..0000000000 --- a/packages/venia-concept/src/components/CreateAccount/validators.js +++ /dev/null @@ -1,69 +0,0 @@ -import { RestApi } from '@magento/peregrine'; - -const { request } = RestApi.Magento2; - -const isPasswordComplexEnough = (str = '') => { - const count = { - lower: 0, - upper: 0, - digit: 0, - special: 0 - }; - - for (const char of str) { - if (/[a-z]/.test(char)) count.lower++; - else if (/[A-Z]/.test(char)) count.upper++; - else if (/\d/.test(char)) count.digit++; - else if (/\S/.test(char)) count.special++; - } - - return Object.values(count).filter(Boolean).length >= 3; -}; - -export const validators = new Map() - .set('confirm', (value, values) => { - return value !== values.password ? 'Passwords must match.' : undefined; - }) - .set('email', value => { - const trimmed = (value || '').trim(); - - if (!trimmed) return 'An email address is required.'; - if (!trimmed.includes('@')) return 'A valid email address is required.'; - - return undefined; - }) - .set('firstName', value => { - return !(value || '').trim() ? 'A first name is required.' : undefined; - }) - .set('lastName', value => { - return !(value || '').trim() ? 'A last name is required.' : undefined; - }) - .set('password', value => { - if (!value || value.length < 8) { - return 'A password must contain at least 8 characters.'; - } - if (!isPasswordComplexEnough(value)) { - return 'A password must contain at least 3 of the following: lowercase, uppercase, digits, special characters.'; - } - - return undefined; - }); - -export const asyncValidators = new Map().set('email', async value => { - try { - const body = { - customerEmail: value, - website_id: null - }; - - // response is a boolean - const available = await request('/rest/V1/customers/isEmailAvailable', { - method: 'POST', - body: JSON.stringify(body) - }); - - return !available ? 'This email address is not available.' : null; - } catch (error) { - throw 'An error occurred while looking up this email address.'; - } -}); diff --git a/packages/venia-concept/src/util/__mocks__/formValidators.js b/packages/venia-concept/src/util/__mocks__/formValidators.js new file mode 100644 index 0000000000..c7137ed965 --- /dev/null +++ b/packages/venia-concept/src/util/__mocks__/formValidators.js @@ -0,0 +1,5 @@ +export const validateEmail = jest.fn(); +export const isRequired = jest.fn(); +export const validatePassword = jest.fn(); +export const validateConfirmPassword = jest.fn(); +export const hasLengthAtLeast = jest.fn(); diff --git a/packages/venia-concept/src/util/__tests__/formValidators.spec.js b/packages/venia-concept/src/util/__tests__/formValidators.spec.js index ff11f1805e..020e2f9e02 100644 --- a/packages/venia-concept/src/util/__tests__/formValidators.spec.js +++ b/packages/venia-concept/src/util/__tests__/formValidators.spec.js @@ -127,3 +127,54 @@ describe('validateRegionCode', () => { expect(typeof result).toBe('string'); }); }); + +describe('validatePassword', () => { + test('it returns undefined on success', () => { + const result = validators.validatePassword('123qwe_+*'); + + expect(result).toBeUndefined(); + }); + + test('it returns a string on failure', () => { + const result = validators.validatePassword('1111'); + + expect(typeof result).toBe('string'); + }); +}); + +describe('validateConfirmPassword', () => { + test('it returns undefined on success', () => { + const values = { + password: 'qwerty12345' + }; + const password = 'qwerty12345'; + const result = validators.validateConfirmPassword(password, values); + + expect(result).toBeUndefined(); + }); + + test('it returns undefined on success with a password key', () => { + const values = { + password_key: 'qwerty12345' + }; + const password = 'qwerty12345'; + const passwordKey = 'password_key'; + const result = validators.validateConfirmPassword( + password, + values, + passwordKey + ); + + expect(result).toBeUndefined(); + }); + + test('it returns a string on failure', () => { + const values = { + password: 'qwertz12345' + }; + const password = 'qwerty12345'; + const result = validators.validateConfirmPassword(password, values); + + expect(typeof result).toBe('string'); + }); +}); diff --git a/packages/venia-concept/src/util/formValidators.js b/packages/venia-concept/src/util/formValidators.js index c88e0e3f06..560d5e2008 100644 --- a/packages/venia-concept/src/util/formValidators.js +++ b/packages/venia-concept/src/util/formValidators.js @@ -63,3 +63,33 @@ export const validateRegionCode = (value, values, countries) => { return SUCCESS; }; + +export const validatePassword = value => { + const count = { + lower: 0, + upper: 0, + digit: 0, + special: 0 + }; + + for (const char of value) { + if (/[a-z]/.test(char)) count.lower++; + else if (/[A-Z]/.test(char)) count.upper++; + else if (/\d/.test(char)) count.digit++; + else if (/\S/.test(char)) count.special++; + } + + if (Object.values(count).filter(Boolean).length < 3) { + return 'A password must contain at least 3 of the following: lowercase, uppercase, digits, special characters.'; + } + + return SUCCESS; +}; + +export const validateConfirmPassword = ( + value, + values, + passwordKey = 'password' +) => { + return value === values[passwordKey] ? SUCCESS : 'Passwords must match.'; +};