From 21787af28b0acf0b10e687b32376c43748965574 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Wed, 1 Nov 2023 14:18:26 -0400 Subject: [PATCH] Port Login to use redux-toolkit (#2748) --- .vscode/settings.json | 1 + src/components/App/DefaultState.ts | 2 +- .../Login/LoginPage/LoginComponent.tsx | 8 +- src/components/Login/LoginPage/index.ts | 9 +- .../LoginPage/tests/LoginComponent.test.tsx | 5 +- src/components/Login/Redux/LoginActions.ts | 98 +++++----- src/components/Login/Redux/LoginReducer.ts | 117 +++++------ src/components/Login/Redux/LoginReduxTypes.ts | 39 ++-- .../Login/Redux/tests/LoginActions.test.tsx | 127 ++++++++++++ .../Login/SignUpPage/SignUpComponent.tsx | 8 +- src/components/Login/SignUpPage/index.ts | 5 +- .../SignUpPage/tests/SignUpComponent.test.tsx | 9 +- .../Login/tests/LoginActions.test.tsx | 183 ------------------ .../Login/tests/LoginReducer.test.tsx | 131 ------------- .../ProjectInvite/ProjectInvite.tsx | 10 +- src/rootReducer.ts | 2 +- 16 files changed, 266 insertions(+), 488 deletions(-) create mode 100644 src/components/Login/Redux/tests/LoginActions.test.tsx delete mode 100644 src/components/Login/tests/LoginActions.test.tsx delete mode 100644 src/components/Login/tests/LoginReducer.test.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 328f2aa278..baa9e6cdb4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "recaptcha", "reportgenerator", "sched", + "signup", "sillsdev", "Sldr", "subtag", diff --git a/src/components/App/DefaultState.ts b/src/components/App/DefaultState.ts index 1294ddf45d..5e981553ad 100644 --- a/src/components/App/DefaultState.ts +++ b/src/components/App/DefaultState.ts @@ -1,5 +1,5 @@ import { defaultState as goalTimelineState } from "components/GoalTimeline/DefaultState"; -import { defaultState as loginState } from "components/Login/Redux/LoginReducer"; +import { defaultState as loginState } from "components/Login/Redux/LoginReduxTypes"; import { defaultState as currentProjectState } from "components/Project/ProjectReduxTypes"; import { defaultState as exportProjectState } from "components/ProjectExport/Redux/ExportProjectReduxTypes"; import { defaultState as pronunciationsState } from "components/Pronunciations/Redux/PronunciationsReduxTypes"; diff --git a/src/components/Login/LoginPage/LoginComponent.tsx b/src/components/Login/LoginPage/LoginComponent.tsx index 3291d02cff..12e3084cbc 100644 --- a/src/components/Login/LoginPage/LoginComponent.tsx +++ b/src/components/Login/LoginPage/LoginComponent.tsx @@ -16,6 +16,7 @@ import { BannerType } from "api/models"; import { getBannerText } from "backend"; import router from "browserRouter"; import { LoadingButton } from "components/Buttons"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; import { Path } from "types/path"; import { RuntimeConfig } from "types/runtimeConfig"; import theme from "types/theme"; @@ -34,8 +35,7 @@ export interface LoginDispatchProps { } export interface LoginStateProps { - loginAttempt?: boolean; - loginFailure?: boolean; + status: LoginStatus; } interface LoginProps @@ -173,7 +173,7 @@ export class Login extends Component { )} {/* "Failed to log in" */} - {this.props.loginFailure && ( + {this.props.status === LoginStatus.Failure && ( { color: "primary", }} disabled={!this.state.isVerified} - loading={this.props.loginAttempt} + loading={this.props.status === LoginStatus.InProgress} > {this.props.t("login.login")} diff --git a/src/components/Login/LoginPage/index.ts b/src/components/Login/LoginPage/index.ts index 992fffffd6..6cec33ab08 100644 --- a/src/components/Login/LoginPage/index.ts +++ b/src/components/Login/LoginPage/index.ts @@ -5,7 +5,7 @@ import Login, { LoginStateProps, } from "components/Login/LoginPage/LoginComponent"; import { - asyncLogin, + asyncLogIn, logoutAndResetStore, } from "components/Login/Redux/LoginActions"; import { reset } from "rootActions"; @@ -13,16 +13,13 @@ import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; function mapStateToProps(state: StoreState): LoginStateProps { - return { - loginAttempt: state.loginState && state.loginState.loginAttempt, - loginFailure: state.loginState && state.loginState.loginFailure, - }; + return { status: state.loginState.loginStatus }; } function mapDispatchToProps(dispatch: StoreStateDispatch): LoginDispatchProps { return { login: (username: string, password: string) => { - dispatch(asyncLogin(username, password)); + dispatch(asyncLogIn(username, password)); }, logout: () => { dispatch(logoutAndResetStore()); diff --git a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx index 45ee12c976..93d89fa73f 100644 --- a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx +++ b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx @@ -8,6 +8,7 @@ import { import "tests/reactI18nextMock"; import Login from "components/Login/LoginPage/LoginComponent"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; jest.mock( "@matt-block/react-recaptcha-v2", @@ -31,7 +32,9 @@ const MOCK_EVENT = { preventDefault: jest.fn(), target: { value: DATA } }; describe("Testing login component", () => { beforeEach(async () => { await act(async () => { - loginMaster = create(); + loginMaster = create( + + ); }); loginHandle = loginMaster.root.findByType(Login); LOGOUT.mockClear(); diff --git a/src/components/Login/Redux/LoginActions.ts b/src/components/Login/Redux/LoginActions.ts index af21bf0376..58b67658f7 100644 --- a/src/components/Login/Redux/LoginActions.ts +++ b/src/components/Login/Redux/LoginActions.ts @@ -1,52 +1,65 @@ +import { PayloadAction } from "@reduxjs/toolkit"; import Hex from "crypto-js/enc-hex"; import sha256 from "crypto-js/sha256"; import * as backend from "backend"; import router from "browserRouter"; import { - LoginActionTypes, - UserAction, -} from "components/Login/Redux/LoginReduxTypes"; + setLoginAttemptAction, + setLoginFailureAction, + setLoginSuccessAction, + setSignupAttemptAction, + 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"; -// thunk action creator -export function asyncLogin(username: string, password: string) { +// Action Creation Functions + +export function loginAttempt(username: string): PayloadAction { + return setLoginAttemptAction(username); +} + +export function loginFailure(error: string): PayloadAction { + return setLoginFailureAction(error); +} + +export function loginSuccess(): PayloadAction { + return setLoginSuccessAction(); +} + +export function signupAttempt(username: string): PayloadAction { + return setSignupAttemptAction(username); +} + +export function signupFailure(error: string): PayloadAction { + return setSignupFailureAction(error); +} + +export function signupSuccess(): PayloadAction { + return setSignupSuccessAction(); +} + +// Dispatch Functions + +export function asyncLogIn(username: string, password: string) { return async (dispatch: StoreStateDispatch) => { dispatch(loginAttempt(username)); await backend .authenticateUser(username, password) .then(async (user) => { - dispatch(loginSuccess(user.username)); + dispatch(loginSuccess()); // hash the user name and use it in analytics.identify const analyticsId = Hex.stringify(sha256(user.id)); analytics.identify(analyticsId); router.navigate(Path.ProjScreen); }) - .catch(() => dispatch(loginFailure(username))); - }; -} - -export function loginAttempt(username: string): UserAction { - return { - type: LoginActionTypes.LOGIN_ATTEMPT, - payload: { username }, - }; -} - -export function loginFailure(username: string): UserAction { - return { - type: LoginActionTypes.LOGIN_FAILURE, - payload: { username }, - }; -} - -export function loginSuccess(username: string): UserAction { - return { - type: LoginActionTypes.LOGIN_SUCCESS, - payload: { username }, + .catch((err) => + dispatch(loginFailure(err.response?.data ?? err.message)) + ); }; } @@ -63,41 +76,20 @@ export function asyncSignUp( password: string ) { return async (dispatch: StoreStateDispatch) => { - dispatch(signUpAttempt(username)); + dispatch(signupAttempt(username)); // Create new user const user = newUser(name, username, password); user.email = email; await backend .addUser(user) .then(() => { - dispatch(signUpSuccess(username)); + dispatch(signupSuccess()); setTimeout(() => { - dispatch(asyncLogin(username, password)); + dispatch(asyncLogIn(username, password)); }, 1000); }) .catch((err) => - dispatch(signUpFailure(err.response?.data ?? err.message)) + dispatch(signupFailure(err.response?.data ?? err.message)) ); }; } - -export function signUpAttempt(username: string): UserAction { - return { - type: LoginActionTypes.SIGN_UP_ATTEMPT, - payload: { username }, - }; -} - -export function signUpFailure(errorMessage: string): UserAction { - return { - type: LoginActionTypes.SIGN_UP_FAILURE, - payload: { username: errorMessage }, - }; -} - -export function signUpSuccess(username: string): UserAction { - return { - type: LoginActionTypes.SIGN_UP_SUCCESS, - payload: { username }, - }; -} diff --git a/src/components/Login/Redux/LoginReducer.ts b/src/components/Login/Redux/LoginReducer.ts index a69bb5d2d2..f1c0ccc846 100644 --- a/src/components/Login/Redux/LoginReducer.ts +++ b/src/components/Login/Redux/LoginReducer.ts @@ -1,72 +1,53 @@ +import { createSlice } from "@reduxjs/toolkit"; + import { - LoginActionTypes, - LoginState, - UserAction, + LoginStatus, + defaultState, } from "components/Login/Redux/LoginReduxTypes"; -import { StoreAction, StoreActionTypes } from "rootActions"; +import { StoreActionTypes } from "rootActions"; + +const loginSlice = createSlice({ + name: "loginState", + initialState: defaultState, + reducers: { + setLoginAttemptAction: (state, action) => { + state.error = ""; + state.loginStatus = LoginStatus.InProgress; + state.signupStatus = LoginStatus.Default; + state.username = action.payload; + }, + setLoginFailureAction: (state, action) => { + state.error = action.payload; + state.loginStatus = LoginStatus.Failure; + }, + setLoginSuccessAction: (state) => { + state.loginStatus = LoginStatus.Success; + }, + setSignupAttemptAction: (state, action) => { + state.error = ""; + state.loginStatus = LoginStatus.Default; + state.signupStatus = LoginStatus.InProgress; + state.username = action.payload; + }, + setSignupFailureAction: (state, action) => { + state.error = action.payload; + state.signupStatus = LoginStatus.Failure; + }, + setSignupSuccessAction: (state) => { + state.signupStatus = LoginStatus.Success; + }, + }, + extraReducers: (builder) => + builder.addCase(StoreActionTypes.RESET, () => defaultState), +}); -export const defaultState: LoginState = { - username: "", - loginAttempt: false, - loginFailure: false, - loginSuccess: false, - signUpAttempt: false, - signUpFailure: "", - signUpSuccess: false, -}; +export const { + setLoginAttemptAction, + setLoginFailureAction, + setLoginSuccessAction, + setSignupAttemptAction, + setSignupFailureAction, + setSignupSuccessAction, +} = loginSlice.actions; -export const loginReducer = ( - state: LoginState = defaultState, //createStore() calls each reducer with undefined state - action: StoreAction | UserAction -): LoginState => { - switch (action.type) { - case LoginActionTypes.LOGIN_ATTEMPT: - return { - ...state, - username: action.payload.username, - loginAttempt: true, - loginSuccess: false, - loginFailure: false, - }; - case LoginActionTypes.LOGIN_FAILURE: - return { - ...state, - username: action.payload.username, - loginAttempt: false, - loginFailure: true, - loginSuccess: false, - }; - case LoginActionTypes.LOGIN_SUCCESS: - return { - ...state, - username: action.payload.username, - loginSuccess: true, - }; - case LoginActionTypes.SIGN_UP_ATTEMPT: - return { - ...state, - username: action.payload.username, - signUpAttempt: true, - signUpFailure: "", - signUpSuccess: false, - }; - case LoginActionTypes.SIGN_UP_SUCCESS: - return { - ...state, - username: action.payload.username, - signUpAttempt: false, - signUpSuccess: true, - }; - case LoginActionTypes.SIGN_UP_FAILURE: - return { - ...state, - signUpAttempt: false, - signUpFailure: action.payload.username, - signUpSuccess: false, - }; - case StoreActionTypes.RESET: - return defaultState; - default: - return state; - } -}; +export default loginSlice.reducer; diff --git a/src/components/Login/Redux/LoginReduxTypes.ts b/src/components/Login/Redux/LoginReduxTypes.ts index 2211ead752..24d18f0d68 100644 --- a/src/components/Login/Redux/LoginReduxTypes.ts +++ b/src/components/Login/Redux/LoginReduxTypes.ts @@ -1,36 +1,25 @@ -export enum LoginActionTypes { - LOGIN_ATTEMPT = "LOGIN_ATTEMPT", - LOGIN_FAILURE = "LOGIN_FAILURE", - LOGIN_SUCCESS = "LOGIN_SUCCESS", - SIGN_UP_ATTEMPT = "SIGN_UP_ATTEMPT", - SIGN_UP_FAILURE = "SIGN_UP_FAILURE", - SIGN_UP_SUCCESS = "SIGN_UP_SUCCESS", +export enum LoginStatus { + Default = "Default", + Failure = "Failure", + InProgress = "InProgress", + Success = "Success", } export interface LoginState { + error: string; + loginStatus: LoginStatus; + signupStatus: LoginStatus; username: string; - loginAttempt: boolean; - loginFailure: boolean; - loginSuccess: boolean; - signUpAttempt: boolean; - signUpFailure: string; - signUpSuccess: boolean; } -export type LoginType = - | typeof LoginActionTypes.LOGIN_ATTEMPT - | typeof LoginActionTypes.LOGIN_FAILURE - | typeof LoginActionTypes.LOGIN_SUCCESS - | typeof LoginActionTypes.SIGN_UP_ATTEMPT - | typeof LoginActionTypes.SIGN_UP_FAILURE - | typeof LoginActionTypes.SIGN_UP_SUCCESS; +export const defaultState: LoginState = { + error: "", + loginStatus: LoginStatus.Default, + signupStatus: LoginStatus.Default, + username: "", +}; export interface LoginData { username: string; password?: string; } - -export interface UserAction { - type: LoginType; - payload: LoginData; -} diff --git a/src/components/Login/Redux/tests/LoginActions.test.tsx b/src/components/Login/Redux/tests/LoginActions.test.tsx new file mode 100644 index 0000000000..48c79e1de4 --- /dev/null +++ b/src/components/Login/Redux/tests/LoginActions.test.tsx @@ -0,0 +1,127 @@ +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 { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; +import { RootState, 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, +}; + +// Preloaded values for store when testing +const persistedDefaultState: PreloadedState = { + ...defaultState, + _persist: { version: 1, rehydrated: false }, +}; + +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); + }); + }); + + 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/SignUpPage/SignUpComponent.tsx b/src/components/Login/SignUpPage/SignUpComponent.tsx index d06f888c6e..c27ab63e66 100644 --- a/src/components/Login/SignUpPage/SignUpComponent.tsx +++ b/src/components/Login/SignUpPage/SignUpComponent.tsx @@ -13,6 +13,7 @@ import { withTranslation, WithTranslation } from "react-i18next"; import router from "browserRouter"; import { LoadingDoneButton } from "components/Buttons"; import { captchaStyle } from "components/Login/LoginPage/LoginComponent"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; import { Path } from "types/path"; import { RuntimeConfig } from "types/runtimeConfig"; import { @@ -38,9 +39,8 @@ interface SignUpDispatchProps { } export interface SignUpStateProps { - inProgress?: boolean; - success?: boolean; failureMessage: string; + status: LoginStatus; } interface SignUpProps @@ -296,8 +296,8 @@ export class SignUp extends Component { { beforeEach(async () => { await act(async () => { - signUpMaster = create(); + signUpMaster = create( + + ); }); signUpHandle = signUpMaster.root.findByType(SignUp); mockReset.mockClear(); diff --git a/src/components/Login/tests/LoginActions.test.tsx b/src/components/Login/tests/LoginActions.test.tsx deleted file mode 100644 index 587f4762a2..0000000000 --- a/src/components/Login/tests/LoginActions.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import configureMockStore from "redux-mock-store"; -import thunk from "redux-thunk"; - -import { User } from "api/models"; -import * as LocalStorage from "backend/localStorage"; -import * as LoginAction from "components/Login/Redux/LoginActions"; -import * as LoginReducer from "components/Login/Redux/LoginReducer"; -import { - LoginActionTypes, - LoginType, - UserAction, -} from "components/Login/Redux/LoginReduxTypes"; -import * as RootAction from "rootActions"; -import { newUser } from "types/user"; - -jest.mock("backend", () => ({ - addUser: (user: User) => mockAddUser(user), - authenticateUser: (username: string, password: string) => - mockAuthenticateUser(username, password), -})); - -// 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 createMockStore = configureMockStore([thunk]); -const mockState = LoginReducer.defaultState; - -const mockUser = { - ...newUser("testName", "testUsername", "testPass"), - token: "testToken", - email: "test@e.mail", -}; -const loginAttempt: UserAction = { - type: LoginActionTypes.LOGIN_ATTEMPT, - payload: { username: mockUser.username }, -}; -const loginFailure: UserAction = { - type: LoginActionTypes.LOGIN_FAILURE, - payload: { username: mockUser.username }, -}; -const loginSuccess: UserAction = { - type: LoginActionTypes.LOGIN_SUCCESS, - payload: { username: mockUser.username }, -}; -const reset: RootAction.StoreAction = { - type: RootAction.StoreActionTypes.RESET, -}; -const signUpAttempt: UserAction = { - type: LoginActionTypes.SIGN_UP_ATTEMPT, - payload: { username: mockUser.username }, -}; -const signUpFailure: UserAction = { - type: LoginActionTypes.SIGN_UP_FAILURE, - payload: { username: mockUser.username }, -}; -const signUpSuccess: UserAction = { - type: LoginActionTypes.SIGN_UP_SUCCESS, - payload: { username: mockUser.username }, -}; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe("LoginAction", () => { - test("sign up returns correct value", () => { - expect(LoginAction.signUpAttempt(mockUser.username)).toEqual(signUpAttempt); - }); - - describe("asyncLogin", () => { - it("login failure correctly affects state", async () => { - mockAuthenticateUser.mockRejectedValue(new Error(mockUser.username)); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncLogin(mockUser.username, mockUser.password) - ); - expect(mockStore.getActions()).toEqual([loginAttempt, loginFailure]); - }); - - it("login success correctly affects state", async () => { - mockAuthenticateUser.mockResolvedValue(mockUser); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncLogin(mockUser.username, mockUser.password) - ); - expect(mockStore.getActions()).toEqual([loginAttempt, loginSuccess]); - }); - }); - - describe("asyncSignUp", () => { - it("sign up failure correctly affects state", async () => { - mockAddUser.mockRejectedValue(new Error(mockUser.username)); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncSignUp( - mockUser.name, - mockUser.username, - mockUser.email, - mockUser.password - ) - ); - expect(mockStore.getActions()).toEqual([signUpAttempt, signUpFailure]); - }); - - it("sign up success correctly affects state", async () => { - mockAddUser.mockResolvedValue(mockUser); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncSignUp( - mockUser.name, - mockUser.username, - mockUser.email, - mockUser.password - ) - ); - expect(mockStore.getActions()).toEqual([signUpAttempt, signUpSuccess]); - }); - }); - - describe("Action creators return correct value.", () => { - test("loginAttempt", () => { - testActionCreatorAgainst( - LoginAction.loginAttempt, - LoginActionTypes.LOGIN_ATTEMPT - ); - }); - - test("loginFailure", () => { - testActionCreatorAgainst( - LoginAction.loginFailure, - LoginActionTypes.LOGIN_FAILURE - ); - }); - - test("loginSuccess", () => { - testActionCreatorAgainst( - LoginAction.loginSuccess, - LoginActionTypes.LOGIN_SUCCESS - ); - }); - - test("signUpAttempt", () => { - testActionCreatorAgainst( - LoginAction.signUpAttempt, - LoginActionTypes.SIGN_UP_ATTEMPT - ); - }); - - test("signUpFailure", () => { - testActionCreatorAgainst( - LoginAction.signUpFailure, - LoginActionTypes.SIGN_UP_FAILURE - ); - }); - - test("signUpSuccess", () => { - testActionCreatorAgainst( - LoginAction.signUpSuccess, - LoginActionTypes.SIGN_UP_SUCCESS - ); - }); - }); - - test("logout creates a proper action", () => { - LocalStorage.setCurrentUser(mockUser); - const mockStore = createMockStore(mockState); - mockStore.dispatch(LoginAction.logoutAndResetStore()); - expect(mockStore.getActions()).toEqual([reset]); - }); -}); - -function testActionCreatorAgainst( - LoginAction: (name: string) => UserAction, - type: LoginType -): void { - expect(LoginAction(mockUser.username)).toEqual({ - type: type, - payload: { username: mockUser.username }, - }); -} diff --git a/src/components/Login/tests/LoginReducer.test.tsx b/src/components/Login/tests/LoginReducer.test.tsx deleted file mode 100644 index 2fa47d7a3b..0000000000 --- a/src/components/Login/tests/LoginReducer.test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import * as LoginReducer from "components/Login/Redux/LoginReducer"; -import { - LoginActionTypes, - LoginData, - LoginState, - UserAction, -} from "components/Login/Redux/LoginReduxTypes"; -import { StoreAction, StoreActionTypes } from "rootActions"; - -const user: LoginData = { - username: "testUsername", - password: "testPassword", -}; - -describe("LoginReducer Tests", () => { - const dummyState: LoginState = { - ...LoginReducer.defaultState, - username: user.username, - loginSuccess: false, - }; - - //The state while attempting to log in - const loginAttemptState: LoginState = { - username: "testUsername", - loginAttempt: true, - loginFailure: false, - loginSuccess: false, - signUpAttempt: false, - signUpFailure: "", - signUpSuccess: false, - }; - - const action: UserAction = { - type: LoginActionTypes.LOGIN_ATTEMPT, - payload: user, - }; - - // Test with no state - test("no state, expecting login attempt", () => { - action.type = LoginActionTypes.LOGIN_ATTEMPT; - expect(LoginReducer.loginReducer(undefined, action)).toEqual( - loginAttemptState - ); - }); - - test("default state, expecting login attempt", () => { - action.type = LoginActionTypes.LOGIN_ATTEMPT; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - loginAttemptState - ); - }); - - test("failed login, expecting no success", () => { - const loginFailureState: LoginState = { - ...LoginReducer.defaultState, - username: user.username, - loginAttempt: false, - loginFailure: true, - loginSuccess: false, - }; - - action.type = LoginActionTypes.LOGIN_FAILURE; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - loginFailureState - ); - }); - - test("default state, expecting sign up", () => { - const resultState: LoginState = { - username: "testUsername", - loginAttempt: false, - loginFailure: false, - loginSuccess: false, - signUpAttempt: true, - signUpFailure: "", - signUpSuccess: false, - }; - action.type = LoginActionTypes.SIGN_UP_ATTEMPT; - - expect(LoginReducer.loginReducer(dummyState, action)).toEqual(resultState); - }); - - test("default state, expecting login success", () => { - const loginSuccessState: LoginState = { - ...dummyState, - username: user.username, - loginSuccess: true, - }; - action.type = LoginActionTypes.LOGIN_SUCCESS; - - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - loginSuccessState - ); - }); - - test("default state, expecting sign up success", () => { - const signUpSuccessState: LoginState = { - ...dummyState, - username: user.username, - signUpAttempt: false, - signUpSuccess: true, - }; - action.type = LoginActionTypes.SIGN_UP_SUCCESS; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - signUpSuccessState - ); - }); - - test("default state, expecting sign up failure", () => { - const signUpFailureState: LoginState = { - ...dummyState, - signUpAttempt: false, - signUpFailure: "testUsername", - signUpSuccess: false, - }; - action.type = LoginActionTypes.SIGN_UP_FAILURE; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - signUpFailureState - ); - }); - - test("non-default state, expecting reset", () => { - const resetAction: StoreAction = { - type: StoreActionTypes.RESET, - }; - - expect(LoginReducer.loginReducer({} as LoginState, resetAction)).toEqual( - LoginReducer.defaultState - ); - }); -}); diff --git a/src/components/ProjectInvite/ProjectInvite.tsx b/src/components/ProjectInvite/ProjectInvite.tsx index fcbe7c3d5c..ee23c4a811 100644 --- a/src/components/ProjectInvite/ProjectInvite.tsx +++ b/src/components/ProjectInvite/ProjectInvite.tsx @@ -10,11 +10,8 @@ import { useAppDispatch, useAppSelector } from "types/hooks"; import { Path } from "types/path"; export default function ProjectInvite(): ReactElement { - const inProgress = useAppSelector((state) => state.loginState.signUpAttempt); - const success = useAppSelector((state) => state.loginState.signUpSuccess); - const failureMessage = useAppSelector( - (state) => state.loginState.signUpFailure - ); + const status = useAppSelector((state) => state.loginState.signupStatus); + const failureMessage = useAppSelector((state) => state.loginState.error); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -38,8 +35,7 @@ export default function ProjectInvite(): ReactElement { return isValidLink ? ( { dispatch(asyncSignUp(name, user, email, password)); diff --git a/src/rootReducer.ts b/src/rootReducer.ts index b0863ed7f0..a617d965fc 100644 --- a/src/rootReducer.ts +++ b/src/rootReducer.ts @@ -1,7 +1,7 @@ import { combineReducers, Reducer } from "redux"; import goalsReducer from "components/GoalTimeline/Redux/GoalReducer"; -import { loginReducer } from "components/Login/Redux/LoginReducer"; +import loginReducer from "components/Login/Redux/LoginReducer"; import { projectReducer } from "components/Project/ProjectReducer"; import exportProjectReducer from "components/ProjectExport/Redux/ExportProjectReducer"; import { pronunciationsReducer } from "components/Pronunciations/Redux/PronunciationsReducer";