Skip to content

Commit

Permalink
add unit tests for selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMelior committed Apr 2, 2024
1 parent 48cb614 commit 813f2d3
Show file tree
Hide file tree
Showing 27 changed files with 308 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { RootState } from '@/app/providers/StoreProvider';
import { User, UserState } from '../../types/user';
import { getUserAuthData } from './getUserAuthData';

describe('getUserAuthData.test', () => {
test('should return initial state', () => {
const authData: User = {
id: 1,
email: 'email@email.com',
username: 'MarkMelior',
};

const initialState: UserState = { authData };

const state: Partial<RootState> = {
user: initialState,
};

expect(getUserAuthData(state as RootState)).toEqual(initialState);
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getUserAuthData(state as RootState)).toEqual(undefined);
});
});
2 changes: 1 addition & 1 deletion src/entities/User/model/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface User {
id: string;
id: number;
email: string;
username?: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/Auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loginByUsername } from './model/service/loginByUsername';
import { loginByUsername } from './model/service/loginByUsername/loginByUsername';
import { loginActions, loginReducer } from './model/slice/loginSlice';
import { LoginState } from './model/types/loginState';
import ModalLogin from './ui/ModalLogin/ModalLogin';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { RootState } from '@/app/providers/StoreProvider';
import { loginInitialState } from '../../slice/loginSlice';
import { LoginState } from '../../types/loginState';
import { getLoginState } from './getLoginState';

describe('getLoginState.test', () => {
test('should return initial state', () => {
const initialState: LoginState = {
username: 'MarkMelior',
password: 'pass',
isLoading: true,
};

const state: Partial<RootState> = {
loginForm: initialState,
};

expect(getLoginState(state as RootState)).toEqual(initialState);
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getLoginState(state as RootState)).toEqual(loginInitialState);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RootState } from '@/app/providers/StoreProvider';
import { loginInitialState } from '../slice/loginSlice';
import { loginInitialState } from '../../slice/loginSlice';

export const getLoginState = (state: RootState) =>
state?.loginForm ?? loginInitialState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { User, userActions } from '@/entities/User';
import { TestAsyncThunk } from '@/shared/lib/tests';
import axios from 'axios';
import { loginByUsername } from './loginByUsername';

jest.mock('axios');
const mockedAxios = jest.mocked(axios);

describe('loginByUsername.test', () => {
test('success login', async () => {
const userValue: User = {
id: 1,
username: 'MarkMelior',
email: 'email@email.com',
};

mockedAxios.post.mockReturnValue(Promise.resolve({ data: userValue }));

const thunk = new TestAsyncThunk(loginByUsername);
const result = await thunk.callThunk({
password: 'pass',
username: 'MarkMelior',
});

expect(thunk.dispatch).toHaveBeenCalledWith(
userActions.setAuthData(userValue),
);
expect(thunk.dispatch).toHaveBeenCalledTimes(3);
expect(mockedAxios.post).toHaveBeenCalled();
expect(result.meta.requestStatus).toBe('fulfilled');
expect(result.payload).toEqual(userValue);
});

test('error login', async () => {
mockedAxios.post.mockReturnValue(Promise.resolve({ status: 403 }));

const thunk = new TestAsyncThunk(loginByUsername);
const result = await thunk.callThunk({
password: 'pass',
username: 'MarkMelior',
});

expect(thunk.dispatch).toHaveBeenCalledTimes(2);
expect(mockedAxios.post).toHaveBeenCalled();
expect(result.meta.requestStatus).toBe('rejected');
expect(result.payload).toBe('error');
});
});
18 changes: 18 additions & 0 deletions src/features/Auth/model/slice/loginSlice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { LoginState } from '../types/loginState';
import { loginActions, loginReducer } from './loginSlice';

describe('loginSlice.test', () => {
test('test set username', () => {
const state: Partial<LoginState> = { username: 'Foren' };
expect(
loginReducer(state as LoginState, loginActions.setUsername('MarkMelior')),
).toEqual({ username: 'MarkMelior' });
});

test('test set password', () => {
const state: Partial<LoginState> = { password: '123' };
expect(
loginReducer(state as LoginState, loginActions.setPassword('pass')),
).toEqual({ password: 'pass' });
});
});
4 changes: 2 additions & 2 deletions src/features/Auth/ui/ModalLogin/ModalLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import { FC, memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getLoginState } from '../../model/selector/getLoginState';
import { loginByUsername } from '../../model/service/loginByUsername';
import { getLoginState } from '../../model/selector/getLoginState/getLoginState';
import { loginByUsername } from '../../model/service/loginByUsername/loginByUsername';
import { loginActions, loginReducer } from '../../model/slice/loginSlice';
import cls from './ModalLogin.module.scss';

Expand Down
24 changes: 24 additions & 0 deletions src/features/Search/model/selector/getQuery/getQuery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { RootState } from '@/app/providers/StoreProvider';
import { searchInitialState } from '../../slice/searchSlice';
import { SearchState } from '../../types/SearchState';
import { getQuery } from './getQuery';

describe('getQuery.test', () => {
test('should return initial state', () => {
const initialState: SearchState = {
query: 'gift',
};

const state: Partial<RootState> = {
search: initialState,
};

expect(getQuery(state as RootState)).toEqual(initialState);
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getQuery(state as RootState)).toEqual(searchInitialState.query);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RootState } from '@/app/providers/StoreProvider';
import { searchInitialState } from '../slice/searchSlice';
import { searchInitialState } from '../../slice/searchSlice';

export const getQuery = (state: RootState) =>
state.search?.query ?? searchInitialState.query;
2 changes: 1 addition & 1 deletion src/features/Search/ui/ModalSearch/ModalSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { FC } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { searchActions } from '../..';
import { getQuery } from '../../model/selector/getQuery';
import { getQuery } from '../../model/selector/getQuery/getQuery';
import { searchReducer } from '../../model/slice/searchSlice';
import cls from './ModalSearch.module.scss';

Expand Down
8 changes: 4 additions & 4 deletions src/features/Settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getSettings } from './model/selector/getSettings';
import { getSettingsCurrency } from './model/selector/getSettingsCurrency';
import { getSettingsOptimization } from './model/selector/getSettingsOptimization';
import { getSettingsSpace } from './model/selector/getSettingsSpace';
import { getSettings } from './model/selector/getSettings/getSettings';
import { getSettingsCurrency } from './model/selector/getSettingsCurrency/getSettingsCurrency';
import { getSettingsOptimization } from './model/selector/getSettingsOptimization/getSettingsOptimization';
import { getSettingsSpace } from './model/selector/getSettingsSpace/getSettingsSpace';
import { settingsActions, settingsReducer } from './model/slice/settingsSlice';
import { SettingsState, SettingsStateKey } from './model/types/settingsState';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { RootState } from '@/app/providers/StoreProvider';
import { SettingsState } from '../../types/settingsState';
import { getSettings } from './getSettings';

describe('getSettings.test', () => {
test('should return initial state', () => {
const initialState: SettingsState = {
space: false,
optimization: false,
currency: 'RUB',
};

const state: Partial<RootState> = {
settings: initialState,
};

expect(getSettings(state as RootState)).toEqual(initialState);
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getSettings(state as RootState)).toEqual(undefined);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { RootState } from '@/app/providers/StoreProvider';
import { SettingsState } from '../../types/settingsState';
import { getSettingsCurrency } from './getSettingsCurrency';

describe('getSettingsCurrency.test', () => {
test('should return initial state', () => {
const initialState: Partial<SettingsState> = {
currency: 'RUB',
};

const state: Partial<RootState> = {
settings: initialState as SettingsState,
};

expect(getSettingsCurrency(state as RootState)).toEqual('RUB');
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getSettingsCurrency(state as RootState)).toEqual(undefined);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { RootState } from '@/app/providers/StoreProvider';
import { SettingsState } from '../../types/settingsState';
import { getSettingsOptimization } from './getSettingsOptimization';

describe('getSettingsOptimization.test', () => {
test('should return initial state', () => {
const initialState: Partial<SettingsState> = {
optimization: true,
};

const state: Partial<RootState> = {
settings: initialState as SettingsState,
};

expect(getSettingsOptimization(state as RootState)).toEqual(true);
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getSettingsOptimization(state as RootState)).toEqual(undefined);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { RootState } from '@/app/providers/StoreProvider';
import { SettingsState } from '../../types/settingsState';
import { getSettingsSpace } from './getSettingsSpace';

describe('getSettingsSpace.test', () => {
test('should return initial state', () => {
const initialState: Partial<SettingsState> = {
space: false,
};

const state: Partial<RootState> = {
settings: initialState as SettingsState,
};

expect(getSettingsSpace(state as RootState)).toEqual(false);
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getSettingsSpace(state as RootState)).toEqual(undefined);
});
});
29 changes: 29 additions & 0 deletions src/features/Sorts/model/selector/getSort/getSort.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { RootState } from '@/app/providers/StoreProvider';
import { initialState as sortInitialState } from '../../slice/sortSlice';
import { SortState } from '../../types/sortType';
import { getSort } from './getSort';

describe('getSort.test', () => {
test('should return initial state', () => {
const initialState: SortState = {
category: ['joke'],
sex: ['male'],
age: ['adult'],
sorting: 'popular',
minPrice: 0,
maxPrice: 14000,
};

const state: Partial<RootState> = {
sort: initialState,
};

expect(getSort(state as RootState)).toEqual(initialState);
});

test('should work with empty state', () => {
const state: Partial<RootState> = {};

expect(getSort(state as RootState)).toEqual(sortInitialState);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RootState } from '@/app/providers/StoreProvider';
import { initialState } from '../slice/sortSlice';
import { initialState } from '../../slice/sortSlice';

export const getSort = (state: RootState) => state.sort ?? initialState;
2 changes: 1 addition & 1 deletion src/features/Sorts/ui/SortButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { categoryButton } from '../model/const/categoryButton';
import { sexButton } from '../model/const/sexButton';
import { sortingButton } from '../model/const/sortingButton';
import { getSortSearchparams } from '../model/features/getSortSearchparams';
import { getSort } from '../model/selector/getSort';
import { getSort } from '../model/selector/getSort/getSort';
import { sortActions } from '../model/slice/sortSlice';
import {
ButtonProps,
Expand Down
2 changes: 1 addition & 1 deletion src/features/Sorts/ui/Sorts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Slider } from '@nextui-org/react';
import { useRouter } from 'next/navigation';
import { FC, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getSort } from '../model/selector/getSort';
import { getSort } from '../model/selector/getSort/getSort';
import { sortActions, sortReducer } from '../model/slice/sortSlice';
import { SortButtons } from './SortButtons';
import cls from './Sorts.module.scss';
Expand Down
25 changes: 25 additions & 0 deletions src/shared/lib/tests/TestAsyncThunk/TestAsyncThunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { RootState } from '@/app/providers/StoreProvider';
import type { AsyncThunkAction } from '@reduxjs/toolkit';

type ActionCreatorType<Return, Arg, RejectedValue> = (
arg: Arg,
) => AsyncThunkAction<Return, Arg, { rejectValue: RejectedValue }>;

export class TestAsyncThunk<Return, Arg, RejectedValue> {
getState: () => RootState;
dispatch: jest.MockedFn<any>;
actionCreator: ActionCreatorType<Return, Arg, RejectedValue>;

constructor(actionCreator: ActionCreatorType<Return, Arg, RejectedValue>) {
this.actionCreator = actionCreator;
this.dispatch = jest.fn();
this.getState = jest.fn();
}

async callThunk(arg: Arg) {
const action = this.actionCreator(arg);
const result = await action(this.dispatch, this.getState, undefined);

return result;
}
}
3 changes: 3 additions & 0 deletions src/shared/lib/tests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TestAsyncThunk } from './TestAsyncThunk/TestAsyncThunk';

export { TestAsyncThunk };

0 comments on commit 813f2d3

Please sign in to comment.