From 1eb0eb442f1da5f5d756adfb50cd2099dc3b7dac Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 2 Nov 2023 09:06:49 -0400 Subject: [PATCH 1/7] Convert to function component and start work on unit tests --- src/components/Buttons/LoadingButton.tsx | 2 +- src/components/Login/Login.tsx | 229 ++++++++++++++++++ .../LoginPage/tests/LoginComponent.test.tsx | 73 ------ src/components/Login/Redux/LoginActions.ts | 7 - .../Login/Redux/tests/LoginActions.test.tsx | 38 +-- src/components/Login/tests/Login.test.tsx | 63 +++++ 6 files changed, 295 insertions(+), 117 deletions(-) create mode 100644 src/components/Login/Login.tsx delete mode 100644 src/components/Login/LoginPage/tests/LoginComponent.test.tsx create mode 100644 src/components/Login/tests/Login.test.tsx diff --git a/src/components/Buttons/LoadingButton.tsx b/src/components/Buttons/LoadingButton.tsx index 11de611d3b..1f00dd5ee7 100644 --- a/src/components/Buttons/LoadingButton.tsx +++ b/src/components/Buttons/LoadingButton.tsx @@ -5,7 +5,7 @@ import React, { ReactElement } from "react"; import { themeColors } from "types/theme"; interface LoadingProps { - buttonProps?: ButtonProps; + buttonProps?: ButtonProps & { "data-testid"?: string }; children?: React.ReactNode; disabled?: boolean; loading?: boolean; diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx new file mode 100644 index 0000000000..87d820408c --- /dev/null +++ b/src/components/Login/Login.tsx @@ -0,0 +1,229 @@ +import ReCaptcha from "@matt-block/react-recaptcha-v2"; +import { Help } from "@mui/icons-material"; +import { + Button, + Card, + CardContent, + Grid, + Link, + TextField, + Typography, +} from "@mui/material"; +import { + ChangeEvent, + FormEvent, + ReactElement, + useEffect, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; + +import { BannerType } from "api/models"; +import { getBannerText } from "backend"; +import router from "browserRouter"; +import { LoadingButton } from "components/Buttons"; +import { asyncLogIn } from "components/Login/Redux/LoginActions"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; +import { reset } from "rootActions"; +import { StoreState } from "types"; +import { useAppDispatch, useAppSelector } from "types/hooks"; +import { Path } from "types/path"; +import { RuntimeConfig } from "types/runtimeConfig"; +import theme from "types/theme"; +import { openUserGuide } from "utilities/pathUtilities"; + +export enum LoginIds { + ButtonLogIn = "login-log-in-button", + ButtonSignUp = "login-sign-up-button", + ButtonUserGuide = "login-user-guide-button", + DivCaptcha = "login-captcha-div", + FieldPassword = "login-password-field", + FieldUsername = "login-username-field", + Form = "login-form", +} + +/** The login page (also doubles as a logout page) */ +export default function Login(): ReactElement { + const dispatch = useAppDispatch(); + + const status = useAppSelector( + (state: StoreState) => state.loginState.loginStatus + ); + + const [banner, setBanner] = useState(""); + const [isVerified, setIsVerified] = useState( + !RuntimeConfig.getInstance().captchaRequired() + ); + const [password, setPassword] = useState(""); + const [passwordError, setPasswordError] = useState(false); + const [username, setUsername] = useState(""); + const [usernameError, setUsernameError] = useState(false); + + const { t } = useTranslation(); + + useEffect(() => { + dispatch(reset()); + getBannerText(BannerType.Login).then(setBanner); + }, [dispatch]); + + const handleUpdatePassword = ( + e: ChangeEvent + ): void => setPassword(e.target.value); + + const handleUpdateUsername = ( + e: ChangeEvent + ): void => setUsername(e.target.value); + + const login = (e: FormEvent): void => { + e.preventDefault(); + const p = password.trim(); + const u = username.trim(); + setPasswordError(!p); + setUsernameError(!u); + if (p && u) { + dispatch(asyncLogIn(u, p)); + } + }; + + return ( + + +
+ + {/* Title */} + + {t("login.title")} + + + {/* Username field */} + + + {/* Password field */} + + + {/* "Forgot password?" link to reset password */} + {RuntimeConfig.getInstance().emailServicesEnabled() && ( + + router.navigate(Path.PwRequest)} + underline="hover" + variant="subtitle2" + > + {t("login.forgotPassword")} + + + )} + + {/* "Failed to log in" */} + {status === LoginStatus.Failure && ( + + {t("login.failed")} + + )} + + {RuntimeConfig.getInstance().captchaRequired() && ( +
+ + console.error( + "Something went wrong; check your connection." + ) + } + onExpire={() => setIsVerified(false)} + onSuccess={() => setIsVerified(true)} + siteKey={RuntimeConfig.getInstance().captchaSiteKey()} + size="normal" + theme="light" + /> +
+ )} + + {/* User Guide, Sign Up, and Log In buttons */} + + + + + + + + + + + + {t("login.login")} + + + + + {/* Login announcement banner */} + {!!banner && ( + + {banner} + + )} +
+
+
+
+ ); +} diff --git a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx deleted file mode 100644 index 93d89fa73f..0000000000 --- a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - ReactTestInstance, - ReactTestRenderer, - act, - create, -} from "react-test-renderer"; - -import "tests/reactI18nextMock"; - -import Login from "components/Login/LoginPage/LoginComponent"; -import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; - -jest.mock( - "@matt-block/react-recaptcha-v2", - () => - function MockRecaptcha() { - return
Recaptcha
; - } -); -jest.mock("backend", () => ({ - getBannerText: () => Promise.resolve(""), -})); - -jest.mock("browserRouter"); -const LOGOUT = jest.fn(); -let loginMaster: ReactTestRenderer; -let loginHandle: ReactTestInstance; - -const DATA = "stuff"; -const MOCK_EVENT = { preventDefault: jest.fn(), target: { value: DATA } }; - -describe("Testing login component", () => { - beforeEach(async () => { - await act(async () => { - loginMaster = create( - - ); - }); - loginHandle = loginMaster.root.findByType(Login); - LOGOUT.mockClear(); - }); - - // These test whether logging in with a username and password (the strings) should result in errors with the username or password (the booleans) - test("Login: no data", () => { - testLogin("", "", true, true); - }); - - test("Login: no password", () => { - testLogin("Username", "", false, true); - }); - - test("Login: no username", () => { - testLogin("", "Password", true, false); - }); - - test("Login: all fields good", () => { - testLogin("Username", "Password", false, false); - }); -}); - -function testLogin( - username: string, - password: string, - goodUsername: boolean, - goodPassword: boolean -): void { - loginHandle.instance.setState({ username, password }); - loginHandle.instance.login(MOCK_EVENT); - expect(loginHandle.instance.state.error).toEqual({ - username: goodUsername, - password: goodPassword, - }); -} diff --git a/src/components/Login/Redux/LoginActions.ts b/src/components/Login/Redux/LoginActions.ts index 58b67658f7..883c8edea2 100644 --- a/src/components/Login/Redux/LoginActions.ts +++ b/src/components/Login/Redux/LoginActions.ts @@ -12,7 +12,6 @@ import { setSignupFailureAction, setSignupSuccessAction, } from "components/Login/Redux/LoginReducer"; -import { reset } from "rootActions"; import { StoreStateDispatch } from "types/Redux/actions"; import { Path } from "types/path"; import { newUser } from "types/user"; @@ -63,12 +62,6 @@ export function asyncLogIn(username: string, password: string) { }; } -export function logoutAndResetStore() { - return (dispatch: StoreStateDispatch) => { - dispatch(reset()); - }; -} - export function asyncSignUp( name: string, username: string, diff --git a/src/components/Login/Redux/tests/LoginActions.test.tsx b/src/components/Login/Redux/tests/LoginActions.test.tsx index 48c79e1de4..65c90d4e08 100644 --- a/src/components/Login/Redux/tests/LoginActions.test.tsx +++ b/src/components/Login/Redux/tests/LoginActions.test.tsx @@ -1,14 +1,7 @@ -import { PreloadedState } from "redux"; - import { User } from "api/models"; -import { defaultState } from "components/App/DefaultState"; -import { - asyncLogIn, - asyncSignUp, - logoutAndResetStore, -} from "components/Login/Redux/LoginActions"; +import { asyncLogIn, asyncSignUp } from "components/Login/Redux/LoginActions"; import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; -import { RootState, setupStore } from "store"; +import { setupStore } from "store"; import { newUser } from "types/user"; jest.mock("backend", () => ({ @@ -31,12 +24,6 @@ const mockUser = { email: mockEmail, }; -// Preloaded values for store when testing -const persistedDefaultState: PreloadedState = { - ...defaultState, - _persist: { version: 1, rehydrated: false }, -}; - beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); @@ -103,25 +90,4 @@ describe("LoginAction", () => { expect(mockAuthenticateUser).toBeCalledTimes(1); }); }); - - describe("logoutAndResetStore", () => { - it("correctly affects state", async () => { - const nonDefaultState = { - error: "nonempty-string", - loginStatus: LoginStatus.Success, - signupStatus: LoginStatus.Failure, - username: "nonempty-string", - }; - const store = setupStore({ - ...persistedDefaultState, - loginState: nonDefaultState, - }); - store.dispatch(logoutAndResetStore()); - const loginState = store.getState().loginState; - expect(loginState.error).toEqual(""); - expect(loginState.loginStatus).toEqual(LoginStatus.Default); - expect(loginState.signupStatus).toEqual(LoginStatus.Default); - expect(loginState.username).toEqual(""); - }); - }); }); diff --git a/src/components/Login/tests/Login.test.tsx b/src/components/Login/tests/Login.test.tsx new file mode 100644 index 0000000000..fb21b1570a --- /dev/null +++ b/src/components/Login/tests/Login.test.tsx @@ -0,0 +1,63 @@ +import "@testing-library/jest-dom"; +import { act, cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Provider } from "react-redux"; +import configureMockStore from "redux-mock-store"; + +import "tests/reactI18nextMock"; + +import Login, { LoginIds } from "components/Login/Login"; +import { defaultState as loginState } from "components/Login/Redux/LoginReduxTypes"; + +jest.mock( + "@matt-block/react-recaptcha-v2", + () => + function MockRecaptcha() { + return
Recaptcha
; + } +); +jest.mock("backend", () => ({ + getBannerText: () => Promise.resolve(""), +})); +jest.mock("browserRouter"); +jest.mock("components/Login/Redux/LoginActions", () => ({ + asyncLogIn: (...args: any[]) => mockAsyncLogIn(...args), +})); + +const mockAsyncLogIn = jest.fn(); +const mockStore = configureMockStore()({ loginState }); + +const renderLogin = async (): Promise => { + await act(async () => { + render( + + + + ); + }); +}; + +beforeEach(async () => { + jest.clearAllMocks(); +}); + +afterEach(cleanup); + +describe("Login", () => { + describe("submit button", () => { + it("errors when no username", async () => { + const agent = userEvent.setup(); + await renderLogin(); + const field = screen.getByTestId(LoginIds.FieldPassword); + await act(async () => { + await agent.type(field, "?"); + await userEvent.click(screen.getByTestId(LoginIds.ButtonLogIn)); + }); + expect(mockAsyncLogIn).not.toBeCalled(); + }); + + it("errors when no password", async () => {}); + + it("submits when username and password", async () => {}); + }); +}); From 6af7923019ca288204c7796071a7c4e74b158501 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 2 Nov 2023 15:23:20 -0400 Subject: [PATCH 2/7] Finish Login conversion and tests --- src/components/Login/Captcha.tsx | 28 ++ src/components/Login/Login.tsx | 32 +-- .../Login/LoginPage/LoginComponent.tsx | 258 ------------------ src/components/Login/LoginPage/index.ts | 33 --- .../Login/Redux/tests/LoginActions.test.tsx | 93 ------- .../Login/SignUpPage/SignUpComponent.tsx | 29 +- src/components/Login/tests/Login.test.tsx | 64 ++++- src/types/appRoutes.tsx | 2 +- 8 files changed, 93 insertions(+), 446 deletions(-) create mode 100644 src/components/Login/Captcha.tsx delete mode 100644 src/components/Login/LoginPage/LoginComponent.tsx delete mode 100644 src/components/Login/LoginPage/index.ts delete mode 100644 src/components/Login/Redux/tests/LoginActions.test.tsx diff --git a/src/components/Login/Captcha.tsx b/src/components/Login/Captcha.tsx new file mode 100644 index 0000000000..d50912cf72 --- /dev/null +++ b/src/components/Login/Captcha.tsx @@ -0,0 +1,28 @@ +import ReCaptcha from "@matt-block/react-recaptcha-v2"; +import { Fragment, ReactElement } from "react"; + +import { RuntimeConfig } from "types/runtimeConfig"; + +interface CaptchaProps { + onExpire: () => void; + onSuccess: () => void; +} + +export default function Captcha(props: CaptchaProps): ReactElement { + return RuntimeConfig.getInstance().captchaRequired() ? ( +
+ + console.error("Something went wrong; check your connection.") + } + onExpire={props.onExpire} + onSuccess={props.onSuccess} + siteKey={RuntimeConfig.getInstance().captchaSiteKey()} + size="normal" + theme="light" + /> +
+ ) : ( + + ); +} diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx index 87d820408c..388723f0bc 100644 --- a/src/components/Login/Login.tsx +++ b/src/components/Login/Login.tsx @@ -1,4 +1,3 @@ -import ReCaptcha from "@matt-block/react-recaptcha-v2"; import { Help } from "@mui/icons-material"; import { Button, @@ -22,6 +21,7 @@ import { BannerType } from "api/models"; import { getBannerText } from "backend"; import router from "browserRouter"; import { LoadingButton } from "components/Buttons"; +import Captcha from "components/Login/Captcha"; import { asyncLogIn } from "components/Login/Redux/LoginActions"; import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; import { reset } from "rootActions"; @@ -36,7 +36,6 @@ export enum LoginIds { ButtonLogIn = "login-log-in-button", ButtonSignUp = "login-sign-up-button", ButtonUserGuide = "login-user-guide-button", - DivCaptcha = "login-captcha-div", FieldPassword = "login-password-field", FieldUsername = "login-username-field", Form = "login-form", @@ -88,7 +87,7 @@ export default function Login(): ReactElement { return ( -
+ {/* Title */} @@ -99,7 +98,6 @@ export default function Login(): ReactElement { )} - {RuntimeConfig.getInstance().captchaRequired() && ( -
- - console.error( - "Something went wrong; check your connection." - ) - } - onExpire={() => setIsVerified(false)} - onSuccess={() => setIsVerified(true)} - siteKey={RuntimeConfig.getInstance().captchaSiteKey()} - size="normal" - theme="light" - /> -
- )} + setIsVerified(false)} + onSuccess={() => setIsVerified(true)} + /> {/* User Guide, Sign Up, and Log In buttons */} @@ -198,7 +179,6 @@ export default function Login(): ReactElement { void; - logout: () => void; - reset: () => void; -} - -export interface LoginStateProps { - status: LoginStatus; -} - -interface LoginProps - extends LoginDispatchProps, - LoginStateProps, - WithTranslation {} - -interface LoginState { - username: string; - password: string; - isVerified: boolean; - error: LoginError; - loginBanner: string; -} - -interface LoginError { - username: boolean; - password: boolean; -} - -/** The login page (also doubles as a logout page) */ -export class Login extends Component { - constructor(props: LoginProps) { - super(props); - this.props.logout(); // Loading this page will reset the app, both store and localStorage - - this.state = { - username: "", - password: "", - isVerified: !RuntimeConfig.getInstance().captchaRequired(), - error: { username: false, password: false }, - loginBanner: "", - }; - } - - componentDidMount(): void { - this.props.reset(); - getBannerText(BannerType.Login).then((loginBanner) => - this.setState({ loginBanner }) - ); - } - - /** Updates the state to match the value in a textbox */ - updateField( - e: React.ChangeEvent< - HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement - >, - field: K - ): void { - const value = e.target.value; - - this.setState({ [field]: value } as Pick); - } - - login(e: React.FormEvent): void { - e.preventDefault(); - - const username: string = this.state.username.trim(); - const password: string = this.state.password.trim(); - const error = { ...this.state.error }; - error.username = username === ""; - error.password = password === ""; - if (error.username || error.password) { - this.setState({ error }); - } else if (this.props.login) { - this.props.login(username, password); - } - } - - render(): ReactElement { - return ( - - - this.login(e)}> - - {/* Title */} - - {this.props.t("login.title")} - - - {/* Username field */} - this.updateField(e, "username")} - error={this.state.error["username"]} - helperText={ - this.state.error["username"] - ? this.props.t("login.required") - : undefined - } - variant="outlined" - style={{ width: "100%" }} - margin="normal" - autoFocus - inputProps={{ maxLength: 100 }} - /> - - {/* Password field */} - this.updateField(e, "password")} - error={this.state.error["password"]} - helperText={ - this.state.error["password"] - ? this.props.t("login.required") - : undefined - } - variant="outlined" - style={{ width: "100%" }} - margin="normal" - inputProps={{ maxLength: 100 }} - /> - - {/* "Forgot password?" link to reset password */} - {RuntimeConfig.getInstance().emailServicesEnabled() && ( - - router.navigate(Path.PwRequest)} - variant="subtitle2" - underline="hover" - > - {this.props.t("login.forgotPassword")} - - - )} - - {/* "Failed to log in" */} - {this.props.status === LoginStatus.Failure && ( - - {this.props.t("login.failed")} - - )} - - {RuntimeConfig.getInstance().captchaRequired() && ( -
- this.setState({ isVerified: true })} - onExpire={() => this.setState({ isVerified: false })} - onError={() => - console.error( - "Something went wrong, check your connection." - ) - } - /> -
- )} - - {/* User Guide, Sign Up, and Log In buttons */} - - - - - - - - - - - - {this.props.t("login.login")} - - - - - {/* Login announcement banner */} - {!!this.state.loginBanner && ( - - {this.state.loginBanner} - - )} -
- -
-
- ); - } -} - -export default withTranslation()(Login); diff --git a/src/components/Login/LoginPage/index.ts b/src/components/Login/LoginPage/index.ts deleted file mode 100644 index 6cec33ab08..0000000000 --- a/src/components/Login/LoginPage/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { connect } from "react-redux"; - -import Login, { - LoginDispatchProps, - LoginStateProps, -} from "components/Login/LoginPage/LoginComponent"; -import { - asyncLogIn, - logoutAndResetStore, -} from "components/Login/Redux/LoginActions"; -import { reset } from "rootActions"; -import { StoreState } from "types"; -import { StoreStateDispatch } from "types/Redux/actions"; - -function mapStateToProps(state: StoreState): LoginStateProps { - return { status: state.loginState.loginStatus }; -} - -function mapDispatchToProps(dispatch: StoreStateDispatch): LoginDispatchProps { - return { - login: (username: string, password: string) => { - dispatch(asyncLogIn(username, password)); - }, - logout: () => { - dispatch(logoutAndResetStore()); - }, - reset: () => { - dispatch(reset()); - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(Login); diff --git a/src/components/Login/Redux/tests/LoginActions.test.tsx b/src/components/Login/Redux/tests/LoginActions.test.tsx deleted file mode 100644 index 65c90d4e08..0000000000 --- a/src/components/Login/Redux/tests/LoginActions.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { User } from "api/models"; -import { asyncLogIn, asyncSignUp } from "components/Login/Redux/LoginActions"; -import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; -import { setupStore } from "store"; -import { newUser } from "types/user"; - -jest.mock("backend", () => ({ - addUser: (user: User) => mockAddUser(user), - authenticateUser: (...args: any[]) => mockAuthenticateUser(...args), -})); - -// Mock the track and identify methods of segment analytics. -global.analytics = { identify: jest.fn(), track: jest.fn() } as any; - -const mockAddUser = jest.fn(); -const mockAuthenticateUser = jest.fn(); - -const mockEmail = "test@e.mail"; -const mockName = "testName"; -const mockPassword = "testPass"; -const mockUsername = "testUsername"; -const mockUser = { - ...newUser(mockName, mockUsername, mockPassword), - email: mockEmail, -}; - -beforeEach(() => { - jest.clearAllMocks(); - jest.useFakeTimers(); -}); - -describe("LoginAction", () => { - describe("asyncLogIn", () => { - it("correctly affects state on failure", async () => { - const store = setupStore(); - mockAuthenticateUser.mockRejectedValueOnce({}); - await store.dispatch(asyncLogIn(mockUsername, mockPassword)); - const loginState = store.getState().loginState; - expect(loginState.error).not.toEqual(""); - expect(loginState.loginStatus).toEqual(LoginStatus.Failure); - expect(loginState.signupStatus).toEqual(LoginStatus.Default); - expect(loginState.username).toEqual(mockUsername); - }); - - it("correctly affects state on success", async () => { - const store = setupStore(); - mockAuthenticateUser.mockResolvedValueOnce(mockUser); - await store.dispatch(asyncLogIn(mockUsername, mockPassword)); - const loginState = store.getState().loginState; - expect(loginState.error).toEqual(""); - expect(loginState.loginStatus).toEqual(LoginStatus.Success); - expect(loginState.signupStatus).toEqual(LoginStatus.Default); - expect(loginState.username).toEqual(mockUsername); - }); - }); - - describe("asyncSignUp", () => { - it("correctly affects state on failure", async () => { - const store = setupStore(); - mockAddUser.mockRejectedValueOnce({}); - await store.dispatch( - asyncSignUp(mockName, mockUsername, mockEmail, mockPassword) - ); - const loginState = store.getState().loginState; - expect(loginState.error).not.toEqual(""); - expect(loginState.loginStatus).toEqual(LoginStatus.Default); - expect(loginState.signupStatus).toEqual(LoginStatus.Failure); - expect(loginState.username).toEqual(mockUsername); - - // A failed signup does not trigger a login. - jest.runAllTimers(); - expect(mockAuthenticateUser).not.toBeCalled(); - }); - - it("correctly affects state on success", async () => { - const store = setupStore(); - mockAddUser.mockResolvedValueOnce({}); - await store.dispatch( - asyncSignUp(mockName, mockUsername, mockEmail, mockPassword) - ); - const loginState = store.getState().loginState; - expect(loginState.error).toEqual(""); - expect(loginState.loginStatus).toEqual(LoginStatus.Default); - expect(loginState.signupStatus).toEqual(LoginStatus.Success); - expect(loginState.username).toEqual(mockUsername); - - // A successful signup triggers a login using `setTimeout`. - mockAuthenticateUser.mockRejectedValueOnce({}); - jest.runAllTimers(); - expect(mockAuthenticateUser).toBeCalledTimes(1); - }); - }); -}); diff --git a/src/components/Login/SignUpPage/SignUpComponent.tsx b/src/components/Login/SignUpPage/SignUpComponent.tsx index c27ab63e66..d74f66c738 100644 --- a/src/components/Login/SignUpPage/SignUpComponent.tsx +++ b/src/components/Login/SignUpPage/SignUpComponent.tsx @@ -1,4 +1,3 @@ -import ReCaptcha from "@matt-block/react-recaptcha-v2"; import { Button, Card, @@ -12,7 +11,7 @@ import { withTranslation, WithTranslation } from "react-i18next"; import router from "browserRouter"; import { LoadingDoneButton } from "components/Buttons"; -import { captchaStyle } from "components/Login/LoginPage/LoginComponent"; +import Captcha from "components/Login/Captcha"; import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; import { Path } from "types/path"; import { RuntimeConfig } from "types/runtimeConfig"; @@ -258,28 +257,12 @@ export class SignUp extends Component {
)} - {RuntimeConfig.getInstance().captchaRequired() && ( -
- this.setState({ isVerified: true })} - onExpire={() => this.setState({ isVerified: false })} - onError={() => - console.error( - "Something went wrong, check your connection." - ) - } - /> -
- )} + this.setState({ isVerified: false })} + onSuccess={() => this.setState({ isVerified: false })} + /> - {/* Sign Up and Login buttons */} + {/* Sign Up and Log In buttons */} + + + + {t("signup.signUp")} + + + +
+ +
+
+ ); +} From abdaf00a08b806616efd4429a15ab20cdc8b66ca Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 3 Nov 2023 09:09:28 -0400 Subject: [PATCH 5/7] Finish conversion and testing --- src/components/InvalidLink/index.tsx | 2 +- src/components/LandingPage/LandingButtons.tsx | 2 +- src/components/Login/Login.tsx | 18 +- .../ProjectInvite.tsx | 21 +- .../Login/SignUpPage/SignUpComponent.tsx | 302 ------------------ src/components/Login/SignUpPage/index.ts | 33 -- .../SignUpPage/tests/SignUpComponent.test.tsx | 124 ------- src/components/Login/Signup.tsx | 144 +++++---- src/components/Login/tests/Login.test.tsx | 20 +- src/components/Login/tests/Signup.test.tsx | 162 ++++++++++ src/types/appRoutes.tsx | 8 +- src/types/path.ts | 2 +- 12 files changed, 266 insertions(+), 572 deletions(-) rename src/components/{ProjectInvite => Login}/ProjectInvite.tsx (55%) delete mode 100644 src/components/Login/SignUpPage/SignUpComponent.tsx delete mode 100644 src/components/Login/SignUpPage/index.ts delete mode 100644 src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx create mode 100644 src/components/Login/tests/Signup.test.tsx diff --git a/src/components/InvalidLink/index.tsx b/src/components/InvalidLink/index.tsx index 3595a6bd3f..ee49f51945 100644 --- a/src/components/InvalidLink/index.tsx +++ b/src/components/InvalidLink/index.tsx @@ -35,7 +35,7 @@ export default function InvalidLink(props: InvalidLinkProps): ReactElement { - - - - {this.props.t("login.signUp")} - - - - - - - - ); - } -} - -export default withTranslation()(SignUp); diff --git a/src/components/Login/SignUpPage/index.ts b/src/components/Login/SignUpPage/index.ts deleted file mode 100644 index b95f6daf7b..0000000000 --- a/src/components/Login/SignUpPage/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { connect } from "react-redux"; - -import { asyncSignUp } from "components/Login/Redux/LoginActions"; -import SignUp, { - SignUpStateProps, -} from "components/Login/SignUpPage/SignUpComponent"; -import { reset } from "rootActions"; -import { StoreState } from "types"; -import { StoreStateDispatch } from "types/Redux/actions"; - -function mapStateToProps(state: StoreState): SignUpStateProps { - return { - failureMessage: state.loginState.error, - status: state.loginState.signupStatus, - }; -} - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function mapDispatchToProps(dispatch: StoreStateDispatch) { - return { - signUp: ( - name: string, - username: string, - email: string, - password: string - ) => { - dispatch(asyncSignUp(name, username, email, password)); - }, - reset: () => dispatch(reset()), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(SignUp); diff --git a/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx b/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx deleted file mode 100644 index 038b49973c..0000000000 --- a/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { - ReactTestInstance, - ReactTestRenderer, - act, - create, -} from "react-test-renderer"; - -import "tests/reactI18nextMock"; - -import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; -import SignUp from "components/Login/SignUpPage/SignUpComponent"; - -jest.mock( - "@matt-block/react-recaptcha-v2", - () => - function MockRecaptcha() { - return
Recaptcha
; - } -); -jest.mock("browserRouter"); - -const mockReset = jest.fn(); -let signUpMaster: ReactTestRenderer; -let signUpHandle: ReactTestInstance; - -const DATA = "stuff"; -const MOCK_EVENT = { preventDefault: jest.fn(), target: { value: DATA } }; - -describe("Testing sign up component", () => { - beforeEach(async () => { - await act(async () => { - signUpMaster = create( - - ); - }); - signUpHandle = signUpMaster.root.findByType(SignUp); - mockReset.mockClear(); - }); - - describe("signUp", () => { - // Test whether various combinations of sign up data should result in errors - test("no data", () => { - testSignUp("", "", "", "", "", true, true, true, true, false); - }); - - test("confirm password doesn't match password", () => { - testSignUp( - "Frodo Baggins", - "underhill", - "a@b.c", - "1234567890", - "1234567899", - false, - false, - false, - false, - true - ); - }); - - test("username too short", () => { - testSignUp( - "Samwise Gamgee", - "sg", - "a@b.c", - "12345678", - "12345678", - false, - true, - false, - false, - false - ); - }); - - test("password too short", () => { - testSignUp( - "Bilbo Baggins", - "bbb", - "a@b.c", - "sting", - "sting", - false, - false, - false, - true, - false - ); - }); - }); -}); - -async function testSignUp( - name: string, - username: string, - email: string, - password: string, - confirmPassword: string, - error_name: boolean, - error_username: boolean, - error_email: boolean, - error_password: boolean, - error_confirmPassword: boolean -): Promise { - signUpHandle.instance.setState({ - name, - username, - email, - password, - confirmPassword, - }); - await signUpHandle.instance.signUp(MOCK_EVENT); - expect(signUpHandle.instance.state.error).toEqual({ - name: error_name, - username: error_username, - email: error_email, - password: error_password, - confirmPassword: error_confirmPassword, - }); -} diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx index 1a65333ed6..1db29ebff0 100644 --- a/src/components/Login/Signup.tsx +++ b/src/components/Login/Signup.tsx @@ -6,7 +6,13 @@ import { TextField, Typography, } from "@mui/material"; -import { ChangeEvent, ReactElement, useEffect, useState } from "react"; +import { + ChangeEvent, + FormEvent, + ReactElement, + useEffect, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import router from "browserRouter"; @@ -50,10 +56,9 @@ const defaultSignupText: SignupText = { [SignupField.Username]: "", }; -export enum SignupIds { +export enum SignupId { ButtonLogIn = "signup-log-in-button", ButtonSignUp = "signup-sign-up-button", - ButtonUserGuide = "signup-user-guide-button", FieldEmail = "signup-email-field", FieldName = "signup-name-field", FieldPassword1 = "signup-password1-field", @@ -71,7 +76,7 @@ interface SignupProps { returnToEmailInvite?: () => void; } -/** The signup page */ +/** The Signup page (also used for ProjectInvite) */ export default function Signup(props: SignupProps): ReactElement { const dispatch = useAppDispatch(); @@ -97,7 +102,7 @@ export default function Signup(props: SignupProps): ReactElement { }, [dispatch]); const errorField = (field: SignupField): void => { - setFieldText({ ...fieldText, [field]: true }); + setFieldError((prev) => ({ ...prev, [field]: true })); }; const checkUsername = (): void => { @@ -114,15 +119,17 @@ export default function Signup(props: SignupProps): ReactElement { setFieldText((prev) => ({ ...prev, ...partialRecord })); }; - const signUp = async (e: React.FormEvent): Promise => { + const signUp = async (e: FormEvent): Promise => { e.preventDefault(); + + // Trim whitespace off fields. const name = fieldText[SignupField.Name].trim(); const username = fieldText[SignupField.Username].trim(); const email = punycode.toUnicode(fieldText[SignupField.Email].trim()); const password1 = fieldText[SignupField.Password1].trim(); const password2 = fieldText[SignupField.Password2].trim(); - // Error checking. + // Check for bad field values. const err: SignupError = { [SignupField.Name]: !name, [SignupField.Email]: !email, @@ -130,6 +137,7 @@ export default function Signup(props: SignupProps): ReactElement { [SignupField.Password2]: password1 !== password2!, [SignupField.Username]: !meetsUsernameRequirements(username), }; + if (Object.values(err).some((e) => e)) { setFieldError(err); } else { @@ -142,107 +150,107 @@ export default function Signup(props: SignupProps): ReactElement { return ( -
signUp(e)}> + {/* Title */} - - {t("signup.signUpNew")} + + {t("login.signUpNew")} {/* Name field */} updateField(e, SignupField.Name)} + autoFocus error={fieldError[SignupField.Name]} helperText={ - fieldError[SignupField.Name] ? t("signup.required") : undefined + fieldError[SignupField.Name] ? t("login.required") : undefined } - variant="outlined" - style={{ width: "100%" }} - margin="normal" + id={SignupId.FieldName} inputProps={{ maxLength: 100 }} + label={t("login.name")} + margin="normal" + onChange={(e) => updateField(e, SignupField.Name)} + required + style={{ width: "100%" }} + value={fieldText[SignupField.Name]} + variant="outlined" /> {/* Username field */} updateField(e, SignupField.Username)} - onBlur={() => checkUsername()} error={fieldError[SignupField.Username]} - helperText={t("signup.usernameRequirements")} - variant="outlined" - style={{ width: "100%" }} - margin="normal" + helperText={t("login.usernameRequirements")} + id={SignupId.FieldUsername} inputProps={{ maxLength: 100 }} + label={t("login.username")} + margin="normal" + onBlur={() => checkUsername()} + onChange={(e) => updateField(e, SignupField.Username)} + required + style={{ width: "100%" }} + value={fieldText[SignupField.Username]} + variant="outlined" /> - {/* email field */} + {/* Email field */} updateField(e, SignupField.Email)} required + style={{ width: "100%" }} type="email" - autoComplete="email" - label={t("signup.email")} value={fieldText[SignupField.Email]} - onChange={(e) => updateField(e, SignupField.Email)} - error={fieldError[SignupField.Email]} variant="outlined" - style={{ width: "100%" }} - margin="normal" - inputProps={{ maxLength: 100 }} /> {/* Password field */} updateField(e, SignupField.Password1)} + required + style={{ width: "100%" }} type="password" value={fieldText[SignupField.Password1]} - onChange={(e) => updateField(e, SignupField.Password1)} - error={fieldError[SignupField.Password1]} - helperText={t("signup.passwordRequirements")} variant="outlined" - style={{ width: "100%" }} - margin="normal" - inputProps={{ maxLength: 100 }} /> {/* Confirm Password field */} updateField(e, SignupField.Password2)} error={fieldError[SignupField.Password2]} helperText={ - fieldError[SignupField.Password1] - ? t("signup.confirmPasswordError") + fieldError[SignupField.Password2] + ? t("login.confirmPasswordError") : undefined } - variant="outlined" - style={{ width: "100%" }} - margin="normal" + id={SignupId.FieldPassword2} inputProps={{ maxLength: 100 }} + label={t("login.confirmPassword")} + margin="normal" + onChange={(e) => updateField(e, SignupField.Password2)} + style={{ width: "100%" }} + type="password" + value={fieldText[SignupField.Password2]} + variant="outlined" /> {/* "Failed to sign up" */} {!!error && ( {t(error)} @@ -257,28 +265,28 @@ export default function Signup(props: SignupProps): ReactElement { - {t("signup.signUp")} + {t("login.signUp")} diff --git a/src/components/Login/tests/Login.test.tsx b/src/components/Login/tests/Login.test.tsx index 95c98e2dd8..35dc189d4c 100644 --- a/src/components/Login/tests/Login.test.tsx +++ b/src/components/Login/tests/Login.test.tsx @@ -9,7 +9,7 @@ import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; -import Login, { LoginIds } from "components/Login/Login"; +import Login, { LoginId } from "components/Login/Login"; import { defaultState as loginState } from "components/Login/Redux/LoginReduxTypes"; jest.mock( @@ -59,9 +59,9 @@ describe("Login", () => { describe("submit button", () => { it("errors when no username", async () => { await renderLogin(); - const fieldPass = loginHandle.findByProps({ id: LoginIds.FieldPassword }); - const fieldUser = loginHandle.findByProps({ id: LoginIds.FieldUsername }); - const form = loginHandle.findByProps({ id: LoginIds.Form }); + const fieldPass = loginHandle.findByProps({ id: LoginId.FieldPassword }); + const fieldUser = loginHandle.findByProps({ id: LoginId.FieldUsername }); + const form = loginHandle.findByProps({ id: LoginId.Form }); await act(async () => { await fieldPass.props.onChange(mockEvent); await form.props.onSubmit(mockEvent); @@ -73,9 +73,9 @@ describe("Login", () => { it("errors when no password", async () => { await renderLogin(); - const fieldPass = loginHandle.findByProps({ id: LoginIds.FieldPassword }); - const fieldUser = loginHandle.findByProps({ id: LoginIds.FieldUsername }); - const form = loginHandle.findByProps({ id: LoginIds.Form }); + const fieldPass = loginHandle.findByProps({ id: LoginId.FieldPassword }); + const fieldUser = loginHandle.findByProps({ id: LoginId.FieldUsername }); + const form = loginHandle.findByProps({ id: LoginId.Form }); await act(async () => { await fieldUser.props.onChange(mockEvent); await form.props.onSubmit(mockEvent); @@ -87,9 +87,9 @@ describe("Login", () => { it("submits when username and password", async () => { await renderLogin(); - const fieldPass = loginHandle.findByProps({ id: LoginIds.FieldPassword }); - const fieldUser = loginHandle.findByProps({ id: LoginIds.FieldUsername }); - const form = loginHandle.findByProps({ id: LoginIds.Form }); + const fieldPass = loginHandle.findByProps({ id: LoginId.FieldPassword }); + const fieldUser = loginHandle.findByProps({ id: LoginId.FieldUsername }); + const form = loginHandle.findByProps({ id: LoginId.Form }); await act(async () => { await fieldPass.props.onChange(mockEvent); await fieldUser.props.onChange(mockEvent); diff --git a/src/components/Login/tests/Signup.test.tsx b/src/components/Login/tests/Signup.test.tsx new file mode 100644 index 0000000000..bf099fa529 --- /dev/null +++ b/src/components/Login/tests/Signup.test.tsx @@ -0,0 +1,162 @@ +import { ChangeEvent, FormEvent } from "react"; +import { Provider } from "react-redux"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; +import configureMockStore from "redux-mock-store"; + +import "tests/reactI18nextMock"; + +import { defaultState as loginState } from "components/Login/Redux/LoginReduxTypes"; +import Signup, { SignupId } from "components/Login/Signup"; + +jest.mock( + "@matt-block/react-recaptcha-v2", + () => + function MockRecaptcha() { + return
Recaptcha
; + } +); +jest.mock("backend", () => ({ + getBannerText: () => Promise.resolve(""), +})); +jest.mock("browserRouter"); +jest.mock("components/Login/Redux/LoginActions", () => ({ + asyncSignUp: (...args: any[]) => mockAsyncSignUp(...args), +})); +jest.mock("types/hooks", () => { + return { + ...jest.requireActual("types/hooks"), + useAppDispatch: () => jest.fn(), + }; +}); + +const mockAsyncSignUp = jest.fn(); +const mockChangeEvent = (text: string): ChangeEvent => + ({ target: { value: text } }) as any as ChangeEvent; +const mockFormEvent: Partial = { preventDefault: jest.fn() }; +const mockStore = configureMockStore()({ loginState }); + +const emailValid = "non@empty.com"; +const nameValid = "Mr. Nonempty"; +const passInvalid = "$hort"; +const passValid = "@-least+8_chars"; +const userInvalid = "no"; +const userValid = "3+ letters long"; + +let signupMaster: ReactTestRenderer; +let signupHandle: ReactTestInstance; + +const renderSignup = async (): Promise => { + await act(async () => { + signupMaster = create( + + + + ); + }); + signupHandle = signupMaster.root.findByType(Signup); +}; + +const typeInField = async (id: SignupId, text: string): Promise => { + const field = signupHandle.findByProps({ id }); + await act(async () => { + await field.props.onChange(mockChangeEvent(text)); + }); +}; + +const submitAndCheckError = async (id?: SignupId): Promise => { + // Submit the form. + const form = signupHandle.findByProps({ id: SignupId.Form }); + await act(async () => { + await form.props.onSubmit(mockFormEvent); + }); + + // Only the specified field should error. + Object.values(SignupId).forEach((val) => { + const field = signupHandle.findByProps({ id: val }); + if (val === id) { + expect(field.props.error).toBeTruthy(); + } else { + expect(field.props.error).toBeFalsy(); + } + }); + + // Expect signUp only when no field expected to error. + if (id === undefined) { + expect(mockAsyncSignUp).toBeCalled(); + } else { + expect(mockAsyncSignUp).not.toBeCalled(); + } +}; + +beforeEach(async () => { + jest.clearAllMocks(); +}); + +describe("Signup", () => { + describe("submit button", () => { + it("errors when email blank", async () => { + await renderSignup(); + await typeInField(SignupId.FieldEmail, ""); + await typeInField(SignupId.FieldName, nameValid); + await typeInField(SignupId.FieldPassword1, passValid); + await typeInField(SignupId.FieldPassword2, passValid); + await typeInField(SignupId.FieldUsername, userValid); + await submitAndCheckError(SignupId.FieldEmail); + }); + + it("errors when name blank", async () => { + await renderSignup(); + await typeInField(SignupId.FieldEmail, emailValid); + await typeInField(SignupId.FieldName, ""); + await typeInField(SignupId.FieldPassword1, passValid); + await typeInField(SignupId.FieldPassword2, passValid); + await typeInField(SignupId.FieldUsername, userValid); + await submitAndCheckError(SignupId.FieldName); + }); + + it("errors when password too short", async () => { + await renderSignup(); + await typeInField(SignupId.FieldEmail, emailValid); + await typeInField(SignupId.FieldName, nameValid); + await typeInField(SignupId.FieldPassword1, passInvalid); + await typeInField(SignupId.FieldPassword2, passInvalid); + await typeInField(SignupId.FieldUsername, userValid); + await submitAndCheckError(SignupId.FieldPassword1); + }); + + it("errors when passwords don't match", async () => { + await renderSignup(); + await typeInField(SignupId.FieldEmail, emailValid); + await typeInField(SignupId.FieldName, nameValid); + await typeInField(SignupId.FieldPassword1, passValid); + await typeInField(SignupId.FieldPassword2, `${passValid}++`); + await typeInField(SignupId.FieldUsername, userValid); + await submitAndCheckError(SignupId.FieldPassword2); + }); + + it("errors when username too short", async () => { + await renderSignup(); + await typeInField(SignupId.FieldEmail, emailValid); + await typeInField(SignupId.FieldName, nameValid); + await typeInField(SignupId.FieldPassword1, passValid); + await typeInField(SignupId.FieldPassword2, passValid); + await typeInField(SignupId.FieldUsername, userInvalid); + await submitAndCheckError(SignupId.FieldUsername); + }); + + it("submits when all fields valid", async () => { + await renderSignup(); + await typeInField(SignupId.FieldEmail, emailValid); + await typeInField(SignupId.FieldName, nameValid); + await typeInField(SignupId.FieldPassword1, passValid); + await typeInField(SignupId.FieldPassword2, passValid); + await typeInField(SignupId.FieldUsername, userValid); + await submitAndCheckError(); + }); + }); +}); diff --git a/src/types/appRoutes.tsx b/src/types/appRoutes.tsx index 7b36818636..5ead464862 100644 --- a/src/types/appRoutes.tsx +++ b/src/types/appRoutes.tsx @@ -3,11 +3,11 @@ import { RouteObject } from "react-router-dom"; import LandingPage from "components/LandingPage"; import Login from "components/Login/Login"; -import SignUp from "components/Login/SignUpPage"; +import ProjectInvite from "components/Login/ProjectInvite"; +import Signup from "components/Login/Signup"; import PageNotFound from "components/PageNotFound/component"; import PasswordRequest from "components/PasswordReset/Request"; import PasswordReset from "components/PasswordReset/ResetPage"; -import ProjectInvite from "components/ProjectInvite/ProjectInvite"; import RequireAuth from "components/RequireAuth"; import { Path } from "types/path"; import { routerPath } from "utilities/pathUtilities"; @@ -33,8 +33,8 @@ export const appRoutes: RouteObject[] = [ element: , }, { - path: Path.SignUp, - element: , + path: Path.Signup, + element: , }, { path: `${Path.PwReset}/:token`, diff --git a/src/types/path.ts b/src/types/path.ts index f9b81996ae..257cfdc799 100644 --- a/src/types/path.ts +++ b/src/types/path.ts @@ -11,7 +11,7 @@ export enum Path { PwRequest = "/forgot/request", PwReset = "/forgot/reset", Root = "/", - SignUp = "/sign-up", + Signup = "/signup", SiteSettings = "/app/site-settings", Statistics = "/app/statistics", UserSettings = "/app/user-settings", From 69dcd53b9f1b81457ae6daa0df04e5a7d7fb1e02 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 3 Nov 2023 09:27:25 -0400 Subject: [PATCH 6/7] Tidy --- src/components/Login/Login.tsx | 21 ++++++++-------- src/components/Login/Signup.tsx | 43 ++++++++++++--------------------- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx index f40da23f77..7de3530dc4 100644 --- a/src/components/Login/Login.tsx +++ b/src/components/Login/Login.tsx @@ -6,6 +6,7 @@ import { Grid, Link, TextField, + TextFieldProps, Typography, } from "@mui/material"; import { @@ -84,6 +85,14 @@ export default function Login(): ReactElement { } }; + const defaultTextFieldProps: TextFieldProps = { + inputProps: { maxLength: 100 }, + margin: "normal", + required: true, + style: { width: "100%" }, + variant: "outlined", + }; + return ( @@ -96,36 +105,28 @@ export default function Login(): ReactElement { {/* Username field */} {/* Password field */} {/* "Forgot password?" link to reset password */} diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx index 1db29ebff0..46dc3d582a 100644 --- a/src/components/Login/Signup.tsx +++ b/src/components/Login/Signup.tsx @@ -4,6 +4,7 @@ import { CardContent, Grid, TextField, + TextFieldProps, Typography, } from "@mui/material"; import { @@ -115,8 +116,7 @@ export default function Signup(props: SignupProps): ReactElement { e: ChangeEvent, field: SignupField ): void => { - const partialRecord = { [field]: e.target.value }; - setFieldText((prev) => ({ ...prev, ...partialRecord })); + setFieldText((prev) => ({ ...prev, [field]: e.target.value })); }; const signUp = async (e: FormEvent): Promise => { @@ -132,10 +132,10 @@ export default function Signup(props: SignupProps): ReactElement { // Check for bad field values. const err: SignupError = { [SignupField.Name]: !name, + [SignupField.Username]: !meetsUsernameRequirements(username), [SignupField.Email]: !email, [SignupField.Password1]: !meetsPasswordRequirements(password1), [SignupField.Password2]: password1 !== password2!, - [SignupField.Username]: !meetsUsernameRequirements(username), }; if (Object.values(err).some((e) => e)) { @@ -147,6 +147,14 @@ export default function Signup(props: SignupProps): ReactElement { } }; + const defaultTextFieldProps: TextFieldProps = { + inputProps: { maxLength: 100 }, + margin: "normal", + required: true, + style: { width: "100%" }, + variant: "outlined", + }; + return ( @@ -159,6 +167,7 @@ export default function Signup(props: SignupProps): ReactElement { {/* Name field */} updateField(e, SignupField.Name)} - required - style={{ width: "100%" }} value={fieldText[SignupField.Name]} - variant="outlined" /> {/* Username field */} checkUsername()} onChange={(e) => updateField(e, SignupField.Username)} - required - style={{ width: "100%" }} value={fieldText[SignupField.Username]} - variant="outlined" /> {/* Email field */} updateField(e, SignupField.Email)} - required - style={{ width: "100%" }} type="email" value={fieldText[SignupField.Email]} - variant="outlined" /> {/* Password field */} updateField(e, SignupField.Password1)} - required - style={{ width: "100%" }} type="password" value={fieldText[SignupField.Password1]} - variant="outlined" /> {/* Confirm Password field */} updateField(e, SignupField.Password2)} - style={{ width: "100%" }} type="password" value={fieldText[SignupField.Password2]} - variant="outlined" /> {/* "Failed to sign up" */} From e8bfe4a7381a79b525e4d9814a6b58aaf181401a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 3 Nov 2023 14:29:57 -0400 Subject: [PATCH 7/7] Remove extra ! and tidy --- src/components/Login/Signup.tsx | 6 ++---- src/components/Login/tests/Signup.test.tsx | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx index 46dc3d582a..3f20468023 100644 --- a/src/components/Login/Signup.tsx +++ b/src/components/Login/Signup.tsx @@ -135,7 +135,7 @@ export default function Signup(props: SignupProps): ReactElement { [SignupField.Username]: !meetsUsernameRequirements(username), [SignupField.Email]: !email, [SignupField.Password1]: !meetsPasswordRequirements(password1), - [SignupField.Password2]: password1 !== password2!, + [SignupField.Password2]: password1 !== password2, }; if (Object.values(err).some((e) => e)) { @@ -255,9 +255,7 @@ export default function Signup(props: SignupProps): ReactElement {