diff --git a/package-lock.json b/package-lock.json index 21d1eb0..d570213 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1447,37 +1447,51 @@ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, "@hapi/address": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.0.1.tgz", + "integrity": "sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } }, "@hapi/bourne": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==" }, + "@hapi/formula": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", + "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==" + }, "@hapi/hoek": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==" + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz", + "integrity": "sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw==" }, "@hapi/joi": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", - "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-17.1.1.tgz", + "integrity": "sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==", "requires": { - "@hapi/address": "2.x.x", - "@hapi/bourne": "1.x.x", - "@hapi/hoek": "8.x.x", - "@hapi/topo": "3.x.x" + "@hapi/address": "^4.0.1", + "@hapi/formula": "^2.0.0", + "@hapi/hoek": "^9.0.0", + "@hapi/pinpoint": "^2.0.0", + "@hapi/topo": "^5.0.0" } }, + "@hapi/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==" + }, "@hapi/topo": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", - "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", "requires": { - "@hapi/hoek": "^8.3.0" + "@hapi/hoek": "^9.0.0" } }, "@jest/console": { @@ -22137,6 +22151,11 @@ "shallowequal": "^1.1.0" } }, + "react-hook-form": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.5.1.tgz", + "integrity": "sha512-OicuetMsvuWaTMZqmOAszsqky56hZIirOA6YSVUGyL4IFDkubgqFoWmdEHFRjv3hly8KHEaPFkhZrRrIkUN4ww==" + }, "react-hotkeys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", @@ -27156,6 +27175,35 @@ "workbox-window": "^4.3.1" }, "dependencies": { + "@hapi/address": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==" + }, + "@hapi/hoek": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==" + }, + "@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "requires": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "requires": { + "@hapi/hoek": "^8.3.0" + } + }, "fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", diff --git a/package.json b/package.json index 8732cb2..eead9f8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "proxy": "http://localhost:8000", "dependencies": { + "@hapi/joi": "17.1.1", "@material-ui/core": "4.5.1", "@material-ui/icons": "4.5.1", "@material-ui/lab": "^4.0.0-alpha.29", @@ -27,6 +28,7 @@ "prop-types": "^15.7.2", "react": "16.11.0", "react-dom": "16.11.0", + "react-hook-form": "5.5.1", "react-router-dom": "5.1.2", "react-scripts": "^3.4.0", "react-select": "3.0.8", diff --git a/src/components/Auth/AuthForm.spec.js b/src/components/Auth/AuthForm.spec.js index 11a1d5a..01783db 100644 --- a/src/components/Auth/AuthForm.spec.js +++ b/src/components/Auth/AuthForm.spec.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, act } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { BrowserRouter } from 'react-router-dom'; import AuthForm from './AuthForm'; @@ -81,6 +81,38 @@ describe('Signup', () => { expect(mockRegisterResponse).toHaveBeenCalledTimes(1); }); + + it('Show required field validation error', async () => { + const { getByText, getByTestId } = render( + + + + ); + + await act(async () => fireEvent.click(getByTestId('submitButton'))); + expect(getByText('Username*').className).toContain('Mui-error'); + expect(getByText('Email*').className).toContain('Mui-error'); + expect(getByText('Password*').className).toContain('Mui-error'); + expect(getByText('First Name*').className).toContain('Mui-error'); + }); + + it('Show username length validation error', async () => { + const { getByText, getByTestId, getByLabelText } = render( + + + + ); + + fireEvent.change(getByLabelText(/username/i), { + target: { value: 'ga' }, + }); + + await act(async () => fireEvent.click(getByTestId('submitButton'))); + + expect( + getByText(/"Username" length must be at least 3 characters long/i) + ).toBeInTheDocument(); + }); }); describe('Login', () => { @@ -98,16 +130,22 @@ describe('Login', () => { username: 'Carolyne.Carter', }, }); - fireEvent.change(getByLabelText(/username/i), { - target: { value: 'Carolyne.Carter' }, - }); - fireEvent.change(getByLabelText(/password/i), { - target: { value: 'password' }, - }); + await act(async () => + fireEvent.change(getByLabelText(/username/i), { + target: { value: 'Carolyne.Carter' }, + }) + ); + + await act(async () => + fireEvent.change(getByLabelText(/password/i), { + target: { value: 'password' }, + }) + ); + const submit = getByRole('button'); - fireEvent.click(submit); - await mockLoginResponse(); + await act(async () => fireEvent.click(submit)); + await act(async () => mockLoginResponse()); expect(mockLoginResponse).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/Auth/SignUpForm.js b/src/components/Auth/SignUpForm.js index 0101b51..10e4108 100644 --- a/src/components/Auth/SignUpForm.js +++ b/src/components/Auth/SignUpForm.js @@ -4,26 +4,22 @@ import { Link, Redirect } from 'react-router-dom'; import { Box, Button, TextField } from '@material-ui/core/'; import axios from 'axios'; import { useAuth } from './AuthContext'; +import { validationResolver, defaultValues } from './SignUpForm.schema'; +import { Form, Field } from '../form'; const SignUpForm = ({ toggleActiveForm }) => { - const [firstName, setFirstName] = useState(null); - const [lastName, setLastName] = useState(null); - const [username, setUsername] = useState(null); - const [email, setEmail] = useState(null); - const [password, setPassword] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [isLoggedIn, setIsLoggedIn] = useState(false); const referer = '/profile'; const auth = useAuth(); - const handleSignup = e => { - e.preventDefault(); + const onSubmit = ({ username, password, firstName, lastName, email }) => { const data = { - username: username, - password: password, + username, + password, + email, first_name: firstName, last_name: lastName, - email: email, }; axios .post('/auth/users/', data) @@ -36,102 +32,111 @@ const SignUpForm = ({ toggleActiveForm }) => { setErrorMessage(Object.values(err.response.data).join('')); }); }; + if (isLoggedIn) { return ; } + + if (auth && auth.authTokens) { + return

Welcome!

; + } + return ( - <> - {(auth && auth.authTokens) || isLoggedIn ? ( -

Welcome!

- ) : ( - <> + + + Create an account + + + + + + + + + {errorMessage && {errorMessage}} + + + + +

+ Already have an account? + {toggleActiveForm ? ( - - Create an account - - setFirstName(e.target.value)} - /> - setLastName(e.target.value)} - /> - setUsername(e.target.value)} - /> - setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> - {errorMessage && {errorMessage}} - - - -

- Already have an account? - {toggleActiveForm ? ( - - Log in - - ) : ( - Log in - )} - . -

+ Log in
- - )} - + ) : ( + Log in + )} + . +

+ ); }; diff --git a/src/components/Auth/SignUpForm.schema.js b/src/components/Auth/SignUpForm.schema.js new file mode 100644 index 0000000..d836de3 --- /dev/null +++ b/src/components/Auth/SignUpForm.schema.js @@ -0,0 +1,39 @@ +import Joi from '@hapi/joi'; +import { createValidationResolver } from '../form'; + +const schema = Joi.object({ + firstName: Joi.string() + .required() + .trim() + .label('First Name'), + lastName: Joi.string() + .allow('') + .trim() + .label('Last Name'), + username: Joi.string() + .alphanum() + .min(3) + .max(30) + .required() + .trim() + .label('Username'), + email: Joi.string() + .email({ tlds: { allow: false } }) + .required() + .trim() + .label('Email'), + password: Joi.string() + .required() + .label('Password'), +}); + +const defaultValues = { + firstName: '', + lastName: '', + username: '', + email: '', + password: '', +}; + +const validationResolver = createValidationResolver(schema); +export { validationResolver, schema, defaultValues }; diff --git a/src/components/Auth/__snapshots__/AuthForm.spec.js.snap b/src/components/Auth/__snapshots__/AuthForm.spec.js.snap index daffbad..e23d6a7 100644 --- a/src/components/Auth/__snapshots__/AuthForm.spec.js.snap +++ b/src/components/Auth/__snapshots__/AuthForm.spec.js.snap @@ -24,7 +24,7 @@ exports[`AuthForm should render accordingly 1`] = ` data-shrink="false" for="first-name" > - First Name + First Name*
@@ -69,6 +70,7 @@ exports[`AuthForm should render accordingly 1`] = ` aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense" id="last-name" + name="lastName" type="text" value="" /> @@ -92,16 +94,11 @@ exports[`AuthForm should render accordingly 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
@@ -134,16 +131,11 @@ exports[`AuthForm should render accordingly 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
@@ -219,6 +206,7 @@ exports[`AuthForm should render accordingly 1`] = ` >