Skip to content

Commit

Permalink
day 48
Browse files Browse the repository at this point in the history
  • Loading branch information
famovkin committed Nov 19, 2023
1 parent 2eda47a commit 371e0cd
Show file tree
Hide file tree
Showing 25 changed files with 67 additions and 52 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ module.exports = {
'react-hooks/exhaustive-deps': 'error',
'no-param-reassign': 'off',
'no-autofocus': 'off',
'no-undef': 'off',
},
settings: {
react: {
Expand Down
4 changes: 3 additions & 1 deletion src/app/providers/StoreProvider/config/StateSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CounterSchema } from 'entities/Counter';
import { ProfileSchema } from 'entities/Profile';
import { UserSchema } from 'entities/User';
import { LoginSchema } from 'features/AuthByUsername';
import { Dispatch } from 'redux';
import { NavigateOptions, To } from 'react-router-dom';

export interface StateSchema {
Expand All @@ -35,9 +36,10 @@ export interface ReduxStoreWithManager extends EnhancedStore<StateSchema> {

export interface ThunkExtraArg {
api: AxiosInstance,
navigate: (to: To, options?: NavigateOptions) => void,
navigate?: (to: To, options?: NavigateOptions) => void,
}
export interface ThunkConfig<T> {
rejectValue: T,
extra: ThunkExtraArg,
dispatch?: Dispatch,
}
16 changes: 10 additions & 6 deletions src/app/providers/StoreProvider/config/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CombinedState,
configureStore,
getDefaultMiddleware,

Check warning on line 4 in src/app/providers/StoreProvider/config/store.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

'getDefaultMiddleware' is defined but never used

Check warning on line 4 in src/app/providers/StoreProvider/config/store.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

'getDefaultMiddleware' is defined but never used
ReducersMapObject,
Expand All @@ -7,7 +8,8 @@ import { counterReducer } from 'entities/Counter';
import { userReducer } from 'entities/User';
import { $api } from 'shared/api/api';
import { NavigateOptions, To } from 'react-router-dom';
import { StateSchema } from './StateSchema';
import { Reducer } from 'redux';
import { StateSchema, ThunkExtraArg } from './StateSchema';
import { createReducerManager } from './reducerManager';

export const createReduxStore = (
Expand All @@ -23,16 +25,18 @@ export const createReduxStore = (

const reducerManager = createReducerManager(rootReducer);

const extraArgs: ThunkExtraArg = {
api: $api,
navigate,
};

const store = configureStore({
reducer: reducerManager.reduce,
reducer: reducerManager.reduce as Reducer<CombinedState<StateSchema>>,
devTools: __IS_DEV__,
preloadedState: initialState,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
thunk: {
extraArgument: {
api: $api,
navigate,
},
extraArgument: extraArgs,
},
}),
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/providers/StoreProvider/ui/StoreProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeepPartial, ReducersMapObject } from '@reduxjs/toolkit';
import { ReducersMapObject } from '@reduxjs/toolkit';
import { ReactNode } from 'react';
import { Provider } from 'react-redux';
import { useNavigate } from 'react-router-dom';
Expand Down
6 changes: 3 additions & 3 deletions src/app/providers/ThemeProvider/lib/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ export const useTheme = (): useThemeResult => {
const { theme, setTheme } = useContext(ThemeContext);

useEffect(() => {
document.body.className = theme;
if (theme) document.body.className = theme;
}, [theme]);

const toggleTheme = () => {
const newTheme = theme === Theme.DARK ? Theme.LIGHT : Theme.DARK;
console.log(newTheme);

Check warning on line 18 in src/app/providers/ThemeProvider/lib/useTheme.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

Unexpected console statement
localStorage.setItem(LOCAL_STORAGE_THEME_KEY, newTheme);
setTheme(newTheme);
setTheme?.(newTheme);
document.body.className = newTheme;
};

return {
theme,
theme: theme || Theme.LIGHT,
toggleTheme,
};
};
4 changes: 4 additions & 0 deletions src/app/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ declare module '*.svg' {

declare const __IS_DEV__: boolean;
declare const __API__: string;

type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeepPartial } from '@reduxjs/toolkit';
import { StateSchema } from 'app/providers/StoreProvider';
import { getCounter } from './getCounter';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeepPartial } from '@reduxjs/toolkit';
import { StateSchema } from 'app/providers/StoreProvider';
import { getCounterValue } from './getCounterValue';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { StateSchema } from "app/providers/StoreProvider";
import { StateSchema } from 'app/providers/StoreProvider';

export const getProfileIsLoading = (state: StateSchema) => state.profile?.isLoading;
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeepPartial } from '@reduxjs/toolkit';
import { StateSchema } from 'app/providers/StoreProvider';
import { getLoginError } from './getLoginError';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeepPartial } from '@reduxjs/toolkit';
import { StateSchema } from 'app/providers/StoreProvider';
import { getLoginIsLoading } from './getLoginIsLoading';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeepPartial } from '@reduxjs/toolkit';
import { StateSchema } from 'app/providers/StoreProvider';
import { getLoginPassword } from './getLoginPassword';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeepPartial } from '@reduxjs/toolkit';
import { StateSchema } from 'app/providers/StoreProvider';
import { getLoginUsername } from './getLoginUsername';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import axios from 'axios';
import { userActions } from 'entities/User';
import { TestAsyncThunk } from 'shared/lib/tests/TestAsyncThunk/TestAsyncThunk';
import { loginByUsername } from './loginByUsername';

jest.mock('axios');

const mockedAxios = jest.mocked(axios, true);

// describe('loginByUsername.test', () => {
// let dispatch: Dispatch;
// let getState: () => StateSchema;
Expand Down Expand Up @@ -44,26 +39,26 @@ const mockedAxios = jest.mocked(axios, true);
describe('loginByUsername.test', () => {
test('test success login', async () => {
const userValue = { username: '123', id: '1' };
mockedAxios.post.mockReturnValue(Promise.resolve({ data: userValue }));

const thunk = new TestAsyncThunk(loginByUsername);

thunk.api.post.mockReturnValue(Promise.resolve({ data: userValue }));
const result = await thunk.callThunk({ username: '123', password: '123' });

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

test('test wrong data, 403 error', async () => {
mockedAxios.post.mockReturnValue(Promise.resolve({ status: 403 }));
const thunk = new TestAsyncThunk(loginByUsername);
thunk.api.post.mockReturnValue(Promise.resolve({ status: 403 }));
const result = await thunk.callThunk({ username: '123', password: '123' });
expect(thunk.dispatch).toHaveBeenCalledTimes(2);
expect(mockedAxios.post).toHaveBeenCalled();
expect(thunk.api.post).toHaveBeenCalled();
expect(result.meta.requestStatus).toBe('rejected');
expect(result.payload).toBe('error');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const loginByUsername = createAsyncThunk<

localStorage.setItem(USER_LOCALSTORAGE_KEY, JSON.stringify(response.data));
dispatch(userActions.setAuthData(response.data));
extra.navigate('/about');
extra?.navigate?.('/about');
return response.data;
} catch (e) {
console.log(e);
Expand Down
1 change: 0 additions & 1 deletion src/features/AuthByUsername/model/slice/loginSlice.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeepPartial } from '@reduxjs/toolkit';
import { LoginSchema } from '../types/loginSchema';
import { loginActions, loginReducer } from './loginSlice';

Expand Down
2 changes: 1 addition & 1 deletion src/shared/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { USER_LOCALSTORAGE_KEY } from 'shared/const/localStorage';
export const $api = axios.create({
baseURL: __API__,
headers: {
authorization: localStorage.getItem(USER_LOCALSTORAGE_KEY),
authorization: localStorage.getItem(USER_LOCALSTORAGE_KEY) || '',
},
});
6 changes: 3 additions & 3 deletions src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { DeepPartial, ReducersMapObject } from '@reduxjs/toolkit';
import { StoryFn } from '@storybook/react';
import { StateSchema, StoreProvider } from 'app/providers/StoreProvider';
import { loginReducer } from 'features/AuthByUsername/model/slice/loginSlice';
import { ReducersList } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader';

const defaultAsyncReducers: DeepPartial<ReducersMapObject<StateSchema>> = {
const defaultAsyncReducers: ReducersList = {
loginForm: loginReducer,
};

export const StoreDecorator = (
state: DeepPartial<StateSchema>,
asyncReducers?: DeepPartial<ReducersMapObject<StateSchema>>,
asyncReducers?: ReducersList,
) => (StoreComponent: StoryFn) => (
<StoreProvider initialState={state} asyncReducers={{ ...defaultAsyncReducers, ...asyncReducers }}>
<StoreComponent />
Expand Down
2 changes: 1 addition & 1 deletion src/shared/lib/classNames/classNames.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Mods = Record<string, boolean | string>;
export type Mods = Record<string, boolean | string | undefined>;

export const classNames = (
cls: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ export type ReducersList = {
[name in StateSchemaKey]?: Reducer;
}

type ReducersListEntry = [StateSchemaKey, Reducer]

interface DynamicModuleLoaderProps {
reducers: ReducersList;
removeAfterUnmount?: boolean;
Expand All @@ -26,15 +24,15 @@ export const DynamicModuleLoader: FC<DynamicModuleLoaderProps> = (props): ReactE
const dispatch = useDispatch();

useEffect(() => {
Object.entries(reducers).forEach(([name, reducer]: ReducersListEntry) => {
store.reducerManager.add(name, reducer);
Object.entries(reducers).forEach(([name, reducer]) => {
store.reducerManager.add(name as StateSchemaKey, reducer);
dispatch({ type: `@INIT ${name} reducer` });
});

return () => {
if (removeAfterUnmount) {
Object.entries(reducers).forEach(([name, reducer]: ReducersListEntry) => {
store.reducerManager.remove(name);
Object.entries(reducers).forEach(([name, reducer]) => {
store.reducerManager.remove(name as StateSchemaKey);
dispatch({ type: `@DESTROY ${name} reducer` });
});
}
Expand Down
20 changes: 19 additions & 1 deletion src/shared/lib/tests/TestAsyncThunk/TestAsyncThunk.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import { AsyncThunkAction } from '@reduxjs/toolkit';
import { StateSchema } from 'app/providers/StoreProvider';
import axios, { AxiosStatic } from 'axios';

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

jest.mock('axios');

const mockedAxios = jest.mocked(axios, true);

export class TestAsyncThunk<Return, Arg, RejectedValue> {
dispatch: jest.MockedFn<any>;

getState: () => StateSchema;

actionCreator: ActionCreatorType<Return, Arg, RejectedValue>;

api: jest.MockedFunctionDeep<AxiosStatic>;

navigate: jest.MockedFn<any>;

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

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

return result;
}
Expand Down
3 changes: 1 addition & 2 deletions src/shared/lib/tests/renderComponent/renderComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { render } from '@testing-library/react';
import { ReactNode } from 'react';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter, MemoryRouter } from 'react-router-dom';
import { MemoryRouter } from 'react-router-dom';
import i18nForTest from 'shared/config/i18n/i18nForTest';
import { DeepPartial } from '@reduxjs/toolkit';
import { StateSchema, StoreProvider } from 'app/providers/StoreProvider';

interface renderComponentOptions {
Expand Down
6 changes: 3 additions & 3 deletions src/shared/ui/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ButtonHTMLAttributes, memo, ReactNode } from 'react';
import { classNames } from 'shared/lib/classNames/classNames';
import { classNames, Mods } from 'shared/lib/classNames/classNames';

import cls from './Button.module.scss';

Expand Down Expand Up @@ -30,14 +30,14 @@ export const Button = memo((props: ButtonProps) => {
const {
className,
children,
theme,
theme = ButtonTheme.OUTLINE,
square,
size = ButtonSize.M,
disabled,
...otherProps
} = props;

const mods: Record<string, boolean> = {
const mods: Mods = {
[cls[theme]]: true,
[cls.square]: square,
[cls[size]]: true,
Expand Down
4 changes: 2 additions & 2 deletions src/shared/ui/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const Input = memo((props: InputProps) => {
useEffect(() => {
if (autofocus) {
setIsFocused(true);
ref.current.focus();
ref.current?.focus();
}
}, [autofocus]);

Expand Down Expand Up @@ -72,7 +72,7 @@ export const Input = memo((props: InputProps) => {
onFocus={onFocus}
onBlur={onBlur}
onSelect={onSelect}
ref={ref}
ref={ref as React.RefObject<HTMLInputElement>}
{...otherProps}
/>
{isFocused && (
Expand Down
7 changes: 4 additions & 3 deletions src/shared/ui/Modal/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
useState,
MouseEvent,
useCallback,
MutableRefObject,
} from 'react';
import { classNames } from 'shared/lib/classNames/classNames';
import { classNames, Mods } from 'shared/lib/classNames/classNames';
import { Portal } from 'shared/ui/Portal/Portal';
import cls from './Modal.module.scss';

Expand All @@ -32,7 +33,7 @@ export const Modal = (props: ModalProps) => {
const [isMounted, setIsMounted] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [isOpening, setIsOpening] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const timerRef = useRef() as MutableRefObject<ReturnType<typeof setTimeout>>;
const { theme } = useTheme();

const closeHandler = useCallback(() => {
Expand Down Expand Up @@ -75,7 +76,7 @@ export const Modal = (props: ModalProps) => {

const onContentClick = (e: MouseEvent) => e.stopPropagation();

const mods: Record<string, boolean> = {
const mods: Mods = {
[cls.opened]: isOpen,
[cls.opened]: isOpening,
[cls.isClosing]: isClosing,
Expand Down

0 comments on commit 371e0cd

Please sign in to comment.