diff --git a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4b59aaa789cd62276e130533d5d3dd710747a364 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx @@ -0,0 +1,61 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { StoreType } from '@/redux/store'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { act } from 'react-dom/test-utils'; +import { LoginModal } from './LoginModal.component'; + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <LoginModal /> + </Wrapper>, + ), + { + store, + } + ); +}; + +test('renders LoginModal component', () => { + renderComponent(); + + const loginInput = screen.getByLabelText(/login/i); + const passwordInput = screen.getByLabelText(/password/i); + expect(loginInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); +}); + +test('handles input change correctly', () => { + renderComponent(); + + const loginInput: HTMLInputElement = screen.getByLabelText(/login/i); + const passwordInput: HTMLInputElement = screen.getByLabelText(/password/i); + + fireEvent.change(loginInput, { target: { value: 'testuser' } }); + fireEvent.change(passwordInput, { target: { value: 'testpassword' } }); + + expect(loginInput.value).toBe('testuser'); + expect(passwordInput.value).toBe('testpassword'); +}); + +test('submits form', () => { + renderComponent(); + + const loginInput = screen.getByLabelText(/login/i); + const passwordInput = screen.getByLabelText(/password/i); + const submitButton = screen.getByText(/submit/i); + + fireEvent.change(loginInput, { target: { value: 'testuser' } }); + fireEvent.change(passwordInput, { target: { value: 'testpassword' } }); + act(() => { + submitButton.click(); + }); + + expect(submitButton).toBeDisabled(); +}); diff --git a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx index 30a0f7f0c6f2532b1e1167293cb27fe87748b0d5..354b53377e7fa13900bc8f38b872099caf622514 100644 --- a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx @@ -1,4 +1,6 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { loadingUserSelector } from '@/redux/user/user.selectors'; import { login } from '@/redux/user/user.thunks'; import { Button } from '@/shared/Button'; import Link from 'next/link'; @@ -6,6 +8,8 @@ import React from 'react'; export const LoginModal: React.FC = () => { const dispatch = useAppDispatch(); + const loadingUser = useAppSelector(loadingUserSelector); + const isPending = loadingUser === 'pending'; const [credentials, setCredentials] = React.useState({ login: '', password: '' }); const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => { @@ -15,8 +19,7 @@ export const LoginModal: React.FC = () => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => { e.preventDefault(); - const formBody = new URLSearchParams(credentials); - dispatch(login(formBody)); + dispatch(login(credentials)); }; return ( @@ -51,7 +54,11 @@ export const LoginModal: React.FC = () => { Forgot password? </Link> </div> - <Button type="submit" className="w-full justify-center text-base font-medium"> + <Button + type="submit" + className="w-full justify-center text-base font-medium" + disabled={isPending} + > Submit </Button> </form> diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx index 24ff90f9f55293eff5a1b4cb94a1a70e3a210a9d..d594f01698bd6bc04fc19d67b02dc196a7e22221 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx @@ -2,17 +2,12 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { openLoginModal } from '@/redux/modal/modal.slice'; import { authenticatedUserSelector, loadingUserSelector } from '@/redux/user/user.selectors'; -import { getSessionValid } from '@/redux/user/user.thunks'; import { Button } from '@/shared/Button'; -import { useEffect } from 'react'; export const UserOverlays = (): JSX.Element => { const dispatch = useAppDispatch(); const loadingUser = useAppSelector(loadingUserSelector); const authenticatedUser = useAppSelector(authenticatedUserSelector); - useEffect(() => { - dispatch(getSessionValid()); - }, [dispatch]); const handleLoginClick = (): void => { dispatch(openLoginModal()); diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts index b2c3ef2a92ac89f7d46a76eff3b70a2e44103772..6185d06495dbaa64d61e789a94f8da67db1a7438 100644 --- a/src/redux/root/init.thunks.ts +++ b/src/redux/root/init.thunks.ts @@ -16,6 +16,7 @@ import { } from '../map/map.thunks'; import { getSearchData } from '../search/search.thunks'; import { setPerfectMatch } from '../search/search.slice'; +import { getSessionValid } from '../user/user.thunks'; interface InitializeAppParams { queryData: QueryData; @@ -42,6 +43,9 @@ export const fetchInitialAppData = createAsyncThunk< /** Create tabs for maps / submaps */ dispatch(initOpenedMaps({ queryData })); + // Check if auth token is valid + dispatch(getSessionValid()); + /** Trigger search */ if (queryData.searchValue) { dispatch(setPerfectMatch(queryData.perfectMatch)); diff --git a/src/redux/user/user.reducers.test.ts b/src/redux/user/user.reducers.test.ts index 2784cc8362abd826b0ac28b920ef530188037cb5..0c89c682c219ec1c7013d3079253e94bf704d5c0 100644 --- a/src/redux/user/user.reducers.test.ts +++ b/src/redux/user/user.reducers.test.ts @@ -13,7 +13,10 @@ import userReducer from './user.slice'; const mockedAxiosClient = mockNetworkResponse(); -const CREDENTIALS = new URLSearchParams(); +const CREDENTIALS = { + login: 'test', + password: 'password', +}; const INITIAL_STATE: UserState = { loading: 'idle', diff --git a/src/redux/user/user.thunks.test.ts b/src/redux/user/user.thunks.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..982274d9bd4d6190f7eb3fb4f7196f2ff8b38d70 --- /dev/null +++ b/src/redux/user/user.thunks.test.ts @@ -0,0 +1,53 @@ +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { loginFixture } from '@/models/fixtures/loginFixture'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { apiPath } from '../apiPath'; +import { closeModal } from '../modal/modal.slice'; +import userReducer from './user.slice'; +import { UserState } from './user.types'; +import { login } from './user.thunks'; + +const mockedAxiosClient = mockNetworkResponse(); +const CREDENTIALS = { + login: 'test', + password: 'password', +}; + +describe('login thunk', () => { + let store = {} as ToolkitStoreWithSingleSlice<UserState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('user', userReducer); + }); + + it('dispatches closeModal action on successful login with valid data', async () => { + mockedAxiosClient.onPost(apiPath.postLogin()).reply(HttpStatusCode.Ok, loginFixture); + + const mockDispatch = jest.fn(action => { + if (action.type === closeModal.type) { + expect(action).toEqual(closeModal()); + } + }); + + store.dispatch = mockDispatch; + + await store.dispatch(login(CREDENTIALS)); + }); + + it('does not dispatch closeModal action on failed login with invalid data', async () => { + mockedAxiosClient.onPost(apiPath.postLogin()).reply(HttpStatusCode.NotFound, loginFixture); + + const mockDispatch = jest.fn(action => { + if (action.type === closeModal.type) { + expect(action).not.toEqual(closeModal()); + } + }); + + store.dispatch = mockDispatch; + + await store.dispatch(login(CREDENTIALS)); + }); +}); diff --git a/src/redux/user/user.thunks.ts b/src/redux/user/user.thunks.ts index 13ade320302bfde726341de965583aad1dd90303..9a35ac49dd925c5a057635e7a5209b9ee8402842 100644 --- a/src/redux/user/user.thunks.ts +++ b/src/redux/user/user.thunks.ts @@ -8,8 +8,9 @@ import { closeModal } from '../modal/modal.slice'; export const login = createAsyncThunk( 'user/login', - async (credentials: URLSearchParams, { dispatch }) => { - const response = await axiosInstance.post(apiPath.postLogin(), credentials); + async (credentials: { login: string; password: string }, { dispatch }) => { + const searchParams = new URLSearchParams(credentials); + const response = await axiosInstance.post(apiPath.postLogin(), searchParams); const isDataValid = validateDataUsingZodSchema(response.data, loginSchema); dispatch(closeModal());